// sdl_manager.cpp - Implementació del gestor SDL3 // © 2026 JailDesigner #include "sdl_manager.hpp" #include #include #include #include #include #include "core/config/postfx_config.hpp" #include "core/defaults.hpp" #include "core/input/mouse.hpp" #include "core/rendering/coordinate_transform.hpp" #include "core/system/notifier.hpp" #include "project.h" namespace { auto initWindowAndGpu(SDL_Window** out_window, Rendering::Renderer& gpu_renderer, int width, int height, bool fullscreen, int initial_vsync) -> bool { // Título estático estilo CCAE. El FPS y el estado de VSync los muestra // el DebugOverlay (toggle F11), no la barra de título. const std::string TITLE = std::format("© 2026 {} — JailDesigner", Project::LONG_NAME); SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE; if (fullscreen) { flags = static_cast(flags | SDL_WINDOW_FULLSCREEN); } SDL_Window* window = SDL_CreateWindow(TITLE.c_str(), width, height, flags); if (window == nullptr) { std::cerr << "Error creant finestra: " << SDL_GetError() << '\n'; return false; } if (!fullscreen) { SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); } // Inicializar el FrameRenderer (claim del window + pipeline de líneas). if (!gpu_renderer.init(window, static_cast(Defaults::Game::WIDTH), static_cast(Defaults::Game::HEIGHT))) { std::cerr << "Error inicialitzant GpuFrameRenderer\n"; SDL_DestroyWindow(window); return false; } gpu_renderer.setVSync(initial_vsync != 0); // Cargar parámetros del postpro desde el resource pack. Si el YAML falta // o falla, el loader devuelve los defaults built-in (bloom suave + flicker // sutil + background verde tenue). gpu_renderer.setPostFx(Config::PostFx::load("config/postfx.yaml")); *out_window = window; return true; } } // namespace SDLManager::SDLManager(int width, int height, bool fullscreen, Config::EngineConfig& cfg, std::function on_persist) : cfg_(&cfg), on_persist_(std::move(on_persist)), current_width_(width), current_height_(height), is_fullscreen_(fullscreen), zoom_factor_(static_cast(width) / Defaults::Window::WIDTH), windowed_width_(width), windowed_height_(height) { if (!SDL_Init(SDL_INIT_VIDEO)) { std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n'; return; } calculateMaxWindowSize(); if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, is_fullscreen_, cfg_->rendering.vsync)) { SDL_Quit(); return; } // Aplica l'estat inicial d'antialias des de la config (per defecte ON). gpu_renderer_.setAntialias(cfg_->rendering.antialias != 0); updateViewport(); // En fullscreen: forzar ocultació permanent del cursor. if (is_fullscreen_) { Mouse::setForceHidden(true); } std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_ << " (logic: " << Defaults::Game::WIDTH << "x" << Defaults::Game::HEIGHT << ")"; if (is_fullscreen_) { std::cout << " [FULLSCREEN]"; } std::cout << '\n'; } SDLManager::~SDLManager() { gpu_renderer_.destroy(); if (finestra_ != nullptr) { SDL_DestroyWindow(finestra_); finestra_ = nullptr; } SDL_Quit(); std::cout << "SDL3 netejat correctament" << '\n'; } void SDLManager::calculateMaxWindowSize() { SDL_DisplayID display = SDL_GetPrimaryDisplay(); const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display); if (mode != nullptr) { // Deixar marge de 100px para decoracions de l'OS max_width_ = mode->w - 100; max_height_ = mode->h - 100; std::cout << "Display detectat: " << mode->w << "x" << mode->h << " (max finestra: " << max_width_ << "x" << max_height_ << ")" << '\n'; } else { max_width_ = 1920; max_height_ = 1080; std::cerr << "No s'ha pogut detectar el display, usant fallback: " << max_width_ << "x" << max_height_ << '\n'; } calculateMaxZoom(); } void SDLManager::calculateMaxZoom() { float max_zoom_width = static_cast(max_width_) / Defaults::Window::WIDTH; float max_zoom_height = static_cast(max_height_) / Defaults::Window::HEIGHT; float max_zoom_unrounded = std::min(max_zoom_width, max_zoom_height); max_zoom_ = std::floor(max_zoom_unrounded / Defaults::Window::ZOOM_INCREMENT) * Defaults::Window::ZOOM_INCREMENT; max_zoom_ = std::max(max_zoom_, Defaults::Window::MIN_ZOOM); std::cout << "Max zoom: " << max_zoom_ << "x (display: " << max_width_ << "x" << max_height_ << ")" << '\n'; } void SDLManager::applyZoom(float new_zoom) { new_zoom = std::max(Defaults::Window::MIN_ZOOM, std::min(new_zoom, max_zoom_)); new_zoom = std::round(new_zoom / Defaults::Window::ZOOM_INCREMENT) * Defaults::Window::ZOOM_INCREMENT; if (std::abs(new_zoom - zoom_factor_) < 0.01F) { return; } zoom_factor_ = new_zoom; int new_width = static_cast(std::round(Defaults::Window::WIDTH * zoom_factor_)); int new_height = static_cast(std::round(Defaults::Window::HEIGHT * zoom_factor_)); applyWindowSize(new_width, new_height); updateViewport(); windowed_width_ = new_width; windowed_height_ = new_height; cfg_->window.width = new_width; cfg_->window.height = new_height; cfg_->window.zoom_factor = zoom_factor_; std::cout << "Zoom: " << zoom_factor_ << "x (" << new_width << "x" << new_height << ")" << '\n'; } void SDLManager::updateViewport() { // Càlcul de letterbox: el joc es renderitza a 1280×720 lògics, però la // swapchain té la mida física de la finestra. Apliquem un viewport // centrat amb aspect-fit (omple un eix, lletrabox a l'altre). // // IMPORTANT: l'escala del viewport es deriva de la mida física actual, // NO del zoom_factor_. El zoom_factor_ només dimensiona la finestra en // mode windowed (F1/F2). Si l'enllacéssim, en fullscreen el viewport // quedaria capat per max_zoom_ (display-100px) i no ompliria la pantalla. float scale_w = static_cast(current_width_) / Defaults::Game::WIDTH; float scale_h = static_cast(current_height_) / Defaults::Game::HEIGHT; float scale = std::min(scale_w, scale_h); int scaled_width = static_cast(std::round(Defaults::Game::WIDTH * scale)); int scaled_height = static_cast(std::round(Defaults::Game::HEIGHT * scale)); int offset_x = (current_width_ - scaled_width) / 2; int offset_y = (current_height_ - scaled_height) / 2; offset_x = std::max(offset_x, 0); offset_y = std::max(offset_y, 0); gpu_renderer_.setViewport(static_cast(offset_x), static_cast(offset_y), static_cast(scaled_width), static_cast(scaled_height)); std::cout << "Viewport: " << scaled_width << "x" << scaled_height << " @ (" << offset_x << "," << offset_y << ") [scale=" << scale << "]" << '\n'; } void SDLManager::updateRenderingContext() const { Rendering::g_current_scale_factor = zoom_factor_; } void SDLManager::increaseWindowSize() { if (is_fullscreen_) { return; } float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT; applyZoom(new_zoom); if (auto* notifier = System::Notifier::get(); notifier != nullptr) { notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_)); } } void SDLManager::decreaseWindowSize() { if (is_fullscreen_) { return; } float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT; applyZoom(new_zoom); if (auto* notifier = System::Notifier::get(); notifier != nullptr) { notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_)); } } void SDLManager::applyWindowSize(int new_width, int new_height) { int old_x; int old_y; SDL_GetWindowPosition(finestra_, &old_x, &old_y); int old_width = current_width_; int old_height = current_height_; SDL_SetWindowSize(finestra_, new_width, new_height); current_width_ = new_width; current_height_ = new_height; int delta_width = old_width - new_width; int delta_height = old_height - new_height; int new_x = old_x + (delta_width / 2); int new_y = old_y + (delta_height / 2); constexpr int TITLEBAR_HEIGHT = 35; new_x = std::max(new_x, 0); new_y = std::max(new_y, TITLEBAR_HEIGHT); SDL_SetWindowPosition(finestra_, new_x, new_y); updateViewport(); } void SDLManager::toggleFullscreen() { if (!is_fullscreen_) { windowed_width_ = current_width_; windowed_height_ = current_height_; is_fullscreen_ = true; // SDL3: cal seleccionar explícitament el mode "borderless desktop" // (mode=nullptr) abans d'activar el fullscreen. Sense això, el // comportament depèn del mode que tingués la finestra anteriorment. SDL_SetWindowFullscreenMode(finestra_, nullptr); SDL_SetWindowFullscreen(finestra_, true); } else { is_fullscreen_ = false; SDL_SetWindowFullscreen(finestra_, false); applyWindowSize(windowed_width_, windowed_height_); } cfg_->window.fullscreen = is_fullscreen_; Mouse::setForceHidden(is_fullscreen_); if (auto* notifier = System::Notifier::get(); notifier != nullptr) { notifier->notifyInfo(is_fullscreen_ ? "PANTALLA COMPLETA" : "MODE FINESTRA"); } } auto SDLManager::handleWindowEvent(const SDL_Event& event) -> bool { if (event.type == SDL_EVENT_WINDOW_RESIZED) { SDL_GetWindowSize(finestra_, ¤t_width_, ¤t_height_); // En fullscreen el zoom_factor_ no participa del viewport (aspect-fit // sobre la mida física), així que el preservem amb el valor de // windowed per no perdre'l en tornar a windowed. if (!is_fullscreen_) { float new_zoom = static_cast(current_width_) / Defaults::Window::WIDTH; zoom_factor_ = std::max(Defaults::Window::MIN_ZOOM, std::min(new_zoom, max_zoom_)); windowed_width_ = current_width_; windowed_height_ = current_height_; } updateViewport(); std::cout << "Finestra redimensionada: " << current_width_ << "x" << current_height_ << " (zoom ≈" << zoom_factor_ << "x)" << '\n'; return true; } return false; } auto SDLManager::clear(uint8_t r, uint8_t g, uint8_t b) -> bool { // El fondo lo dibuja ahora el shader de postpro (background pulse). El // offscreen se limpia en negro dentro de beginFrame. Los argumentos r/g/b // se mantienen por compatibilidad de API. (void)r; (void)g; (void)b; // beginFrame devuelve false si la swapchain no está disponible (ventana // minimizada, por ejemplo). Propagamos el bool al caller para que pueda // saltarse draw+present ese frame; si no, los vértices se acumulan en // el batch interno sin que nadie los consuma. return gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F); } void SDLManager::present() { gpu_renderer_.endFrame(); } void SDLManager::toggleVSync() { cfg_->rendering.vsync = (cfg_->rendering.vsync == 1) ? 0 : 1; gpu_renderer_.setVSync(cfg_->rendering.vsync != 0); if (on_persist_) { on_persist_(); } if (auto* notifier = System::Notifier::get(); notifier != nullptr) { notifier->notifyInfo(cfg_->rendering.vsync != 0 ? "VSYNC ACTIU" : "VSYNC INACTIU"); } } void SDLManager::toggleAntialias() { cfg_->rendering.antialias = (cfg_->rendering.antialias == 1) ? 0 : 1; gpu_renderer_.setAntialias(cfg_->rendering.antialias != 0); // No persistim: l'AA és toggleable runtime però el seu estat no es // guarda al YAML de moment (decisió volgudament conservadora). if (auto* notifier = System::Notifier::get(); notifier != nullptr) { notifier->notifyInfo(cfg_->rendering.antialias != 0 ? "AA ACTIU" : "AA INACTIU"); } }