Files
orni-attack/source/core/rendering/sdl_manager.cpp
T

397 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// sdl_manager.cpp - Implementació del gestor SDL3
// © 2026 JailDesigner
#include "sdl_manager.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <format>
#include <iostream>
#include "core/config/postfx_config.hpp"
#include "core/defaults/game.hpp"
#include "core/defaults/rendering.hpp"
#include "core/defaults/window.hpp"
#include "core/input/mouse.hpp"
#include "core/locale/locale.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,
int render_width,
int render_height) -> 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<SDL_WindowFlags>(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).
// logical_*: espacio de coordenadas del juego (fijo 1280×720).
// render_*: resolución física del offscreen (configurable vía YAML).
if (!gpu_renderer.init(window,
static_cast<float>(Defaults::Game::WIDTH),
static_cast<float>(Defaults::Game::HEIGHT),
static_cast<float>(render_width),
static_cast<float>(render_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<void()> on_persist)
: cfg_(&cfg),
on_persist_(std::move(on_persist)),
current_width_(width),
current_height_(height),
is_fullscreen_(fullscreen),
zoom_factor_(static_cast<float>(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();
// Validar la resolució de render del config: si no és un preset 16:9
// conegut, fer fallback a 1280×720 i avisar. Això protegeix d'edicions
// manuals invàlides al YAML.
int effective_render_w = cfg_->rendering.render_width;
int effective_render_h = cfg_->rendering.render_height;
if (!Defaults::Rendering::isValidRenderResolution(effective_render_w, effective_render_h)) {
std::cerr << "Resolució de render invàlida (" << effective_render_w << "x"
<< effective_render_h << "), fent fallback a "
<< Defaults::Rendering::RENDER_WIDTH_DEFAULT << "x"
<< Defaults::Rendering::RENDER_HEIGHT_DEFAULT << '\n';
effective_render_w = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
effective_render_h = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
cfg_->rendering.render_width = effective_render_w;
cfg_->rendering.render_height = effective_render_h;
}
if (!initWindowAndGpu(&finestra_,
gpu_renderer_,
current_width_,
current_height_,
is_fullscreen_,
cfg_->rendering.vsync,
effective_render_w,
effective_render_h)) {
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
<< ", render: " << effective_render_w << "x" << effective_render_h
<< ")";
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<float>(max_width_) / Defaults::Window::WIDTH;
float max_zoom_height = static_cast<float>(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<int>(std::round(Defaults::Window::WIDTH * zoom_factor_));
int new_height = static_cast<int>(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<float>(current_width_) / Defaults::Game::WIDTH;
float scale_h = static_cast<float>(current_height_) / Defaults::Game::HEIGHT;
float scale = std::min(scale_w, scale_h);
int scaled_width = static_cast<int>(std::round(Defaults::Game::WIDTH * scale));
int scaled_height = static_cast<int>(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<float>(offset_x),
static_cast<float>(offset_y),
static_cast<float>(scaled_width),
static_cast<float>(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(localeSubstitute(
Locale::get().text("notification.zoom"),
"{z}",
std::format("{:.1f}", 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(localeSubstitute(
Locale::get().text("notification.zoom"),
"{z}",
std::format("{:.1f}", 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(Locale::get().text(is_fullscreen_ ? "notification.fullscreen_on" : "notification.fullscreen_off"));
}
}
auto SDLManager::handleWindowEvent(const SDL_Event& event) -> bool {
if (event.type == SDL_EVENT_WINDOW_RESIZED) {
SDL_GetWindowSize(finestra_, &current_width_, &current_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<float>(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(Locale::get().text(cfg_->rendering.vsync != 0 ? "notification.vsync_on" : "notification.vsync_off"));
}
}
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(Locale::get().text(cfg_->rendering.antialias != 0 ? "notification.antialias_on" : "notification.antialias_off"));
}
}
void SDLManager::togglePostFx() {
const bool NEW_STATE = !gpu_renderer_.isPostFxEnabled();
gpu_renderer_.setPostFxEnabled(NEW_STATE);
// No persistim: el toggle és per A/B testing visual, l'estat per defecte
// del joc continua sent "postfx ON" segons defaults/YAML.
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(Locale::get().text(NEW_STATE ? "notification.postfx_on" : "notification.postfx_off"));
}
}