From 20325ddd5a35505d5f90a3a5c6fcc8dd465c156c Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 19 May 2026 20:29:22 +0200 Subject: [PATCH] presentation: bool integer_scale -> enum PresentationMode (integer_scale|letterbox|stretched|overscan) amb migracio de configs antics --- source/core/rendering/screen.cpp | 113 +++++++++++++----- source/core/rendering/screen.h | 26 ++-- .../core/rendering/sdl3gpu/sdl3gpu_shader.cpp | 55 ++++++--- .../core/rendering/sdl3gpu/sdl3gpu_shader.hpp | 6 +- source/core/rendering/shader_backend.hpp | 11 +- source/game/defaults.hpp | 3 +- source/game/options.cpp | 52 +++++++- source/game/options.hpp | 17 ++- 8 files changed, 214 insertions(+), 69 deletions(-) diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index 9c426a4..d475e9c 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -326,16 +326,27 @@ void Screen::detectMaxZoom() { #endif } -// Establece el escalado entero -void Screen::setIntegerScale(bool enabled) { - if (Options::video.integer_scale == enabled) { return; } - Options::video.integer_scale = enabled; +// Estableix el mode de presentacio del canvas i reaplica el layout +void Screen::setPresentationMode(Options::PresentationMode mode) { + if (Options::video.presentation_mode == mode) { return; } + Options::video.presentation_mode = mode; +#ifndef NO_SHADERS + if (shader_backend_) { + shader_backend_->setPresentationMode(static_cast(mode)); + } +#endif setVideoMode(Options::video.fullscreen); } -// Alterna el escalado entero -void Screen::toggleIntegerScale() { - setIntegerScale(!Options::video.integer_scale); +// Cicla integer_scale -> letterbox -> stretched -> overscan -> integer_scale +void Screen::nextPresentationMode() { + setPresentationMode(Options::nextPresentationMode(Options::video.presentation_mode)); +} + +// Nom curt del mode actual (per a notificacions). Static perque no necessita +// estat d'instancia: nomes consulta Options::video. +auto Screen::getPresentationModeName() -> const char * { + return Options::presentationModeToString(Options::video.presentation_mode); } // Establece el V-Sync @@ -407,39 +418,75 @@ void Screen::applyFullscreenLayout() { computeFullscreenGameRect(); } -// Calcula el rectángulo dest para fullscreen: integer_scale / aspect ratio +// Calcula el rectangle dest segons el PresentationMode actiu. +// INTEGER_SCALE: x sencera maxima (1x, 2x, 3x...) centrada amb barres. +// LETTERBOX: mante aspect ratio, ajusta al menor dels eixos, barres. +// STRETCHED: omple tota la finestra deformant la relacio d'aspecte. +// OVERSCAN: mante aspect ratio omplint la finestra (retalla el sobrant). void Screen::computeFullscreenGameRect() { - if (Options::video.integer_scale) { - // Calcula el tamaño de la escala máxima - int scale = 0; - while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) { - scale++; - } + const float CANVAS_RATIO = static_cast(game_canvas_width_) / static_cast(game_canvas_height_); + const float WINDOW_RATIO = static_cast(window_width_) / static_cast(window_height_); - dest_.w = game_canvas_width_ * scale; - dest_.h = game_canvas_height_ * scale; - dest_.x = (window_width_ - dest_.w) / 2; - dest_.y = (window_height_ - dest_.h) / 2; - } else { - // Manté la relació d'aspecte sense escalat enter (letterbox/pillarbox). - float ratio = (float)game_canvas_width_ / (float)game_canvas_height_; - if ((window_width_ - game_canvas_width_) >= (window_height_ - game_canvas_height_)) { - dest_.h = window_height_; - dest_.w = static_cast(std::lround(window_height_ * ratio)); - dest_.x = (window_width_ - dest_.w) / 2; - dest_.y = (window_height_ - dest_.h) / 2; - } else { + switch (Options::video.presentation_mode) { + case Options::PresentationMode::INTEGER_SCALE: { + int scale = 0; + while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) { + scale++; + } + dest_.w = game_canvas_width_ * scale; + dest_.h = game_canvas_height_ * scale; + break; + } + case Options::PresentationMode::LETTERBOX: { + if (WINDOW_RATIO >= CANVAS_RATIO) { + dest_.h = window_height_; + dest_.w = static_cast(std::lround(window_height_ * CANVAS_RATIO)); + } else { + dest_.w = window_width_; + dest_.h = static_cast(std::lround(window_width_ / CANVAS_RATIO)); + } + break; + } + case Options::PresentationMode::STRETCHED: { dest_.w = window_width_; - dest_.h = static_cast(std::lround(window_width_ / ratio)); - dest_.x = (window_width_ - dest_.w) / 2; - dest_.y = (window_height_ - dest_.h) / 2; + dest_.h = window_height_; + break; + } + case Options::PresentationMode::OVERSCAN: { + // Mante aspect: dimensiona al major dels eixos (l'altre desborda). + if (WINDOW_RATIO >= CANVAS_RATIO) { + dest_.w = window_width_; + dest_.h = static_cast(std::lround(window_width_ / CANVAS_RATIO)); + } else { + dest_.h = window_height_; + dest_.w = static_cast(std::lround(window_height_ * CANVAS_RATIO)); + } + break; } } + dest_.x = (window_width_ - dest_.w) / 2; + dest_.y = (window_height_ - dest_.h) / 2; } -// Aplica la logical presentation y persiste el estado en options +// Aplica la logical presentation segons el PresentationMode actiu (ruta SDL_Renderer fallback). +// La ruta GPU calcula el viewport ella mateixa via computeViewport(). void Screen::applyLogicalPresentation(bool fullscreen) { - SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, SDL_LOGICAL_PRESENTATION_LETTERBOX); + SDL_RendererLogicalPresentation lp = SDL_LOGICAL_PRESENTATION_LETTERBOX; + switch (Options::video.presentation_mode) { + case Options::PresentationMode::INTEGER_SCALE: + lp = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; + break; + case Options::PresentationMode::LETTERBOX: + lp = SDL_LOGICAL_PRESENTATION_LETTERBOX; + break; + case Options::PresentationMode::STRETCHED: + lp = SDL_LOGICAL_PRESENTATION_STRETCH; + break; + case Options::PresentationMode::OVERSCAN: + lp = SDL_LOGICAL_PRESENTATION_OVERSCAN; + break; + } + SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, lp); Options::video.fullscreen = fullscreen; } @@ -545,7 +592,7 @@ void Screen::initShaders() { } } if (shader_backend_->isHardwareAccelerated()) { - shader_backend_->setScaleMode(Options::video.integer_scale); + shader_backend_->setPresentationMode(static_cast(Options::video.presentation_mode)); shader_backend_->setVSync(Options::video.vsync); // Resol els índexs de preset a partir del nom emmagatzemat al config. diff --git a/source/core/rendering/screen.h b/source/core/rendering/screen.h index 52540df..8afe954 100644 --- a/source/core/rendering/screen.h +++ b/source/core/rendering/screen.h @@ -6,7 +6,8 @@ #include // for string #include // for vector -#include "utils/utils.h" // for Color +#include "game/options.hpp" // for Options::PresentationMode +#include "utils/utils.h" // for Color #ifndef NO_SHADERS #include "core/rendering/shader_backend.hpp" // for Rendering::ShaderType @@ -51,17 +52,18 @@ class Screen { void blit(); // Vuelca el contenido del renderizador en pantalla // Video y ventana - void setVideoMode(bool fullscreen); // Establece el modo de video - void toggleVideoMode(); // Cambia entre pantalla completa y ventana - void handleCanvasResized(); // En Emscripten, reaplica setVideoMode tras un cambio del navegador (salida de fullscreen con Esc, rotación). No-op fuera de Emscripten - static void syncFullscreenFlagFromBrowser(bool is_fullscreen); // Sincroniza el flag interno de fullscreen con el estado real del navegador. Debe llamarse antes de diferir handleCanvasResized. No-op fuera de Emscripten - void toggleIntegerScale(); // Alterna el escalado entero - void setIntegerScale(bool enabled); // Establece el escalado entero - void toggleVSync(); // Alterna el V-Sync - void setVSync(bool enabled); // Establece el V-Sync - auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió) - auto incWindowZoom() -> bool; // Aumenta el zoom de la ventana (devuelve true si cambió) - auto setWindowZoom(int zoom) -> bool; // Establece el zoom de la ventana (devuelve true si cambió) + void setVideoMode(bool fullscreen); // Establece el modo de video + void toggleVideoMode(); // Cambia entre pantalla completa y ventana + void handleCanvasResized(); // En Emscripten, reaplica setVideoMode tras un cambio del navegador (salida de fullscreen con Esc, rotación). No-op fuera de Emscripten + static void syncFullscreenFlagFromBrowser(bool is_fullscreen); // Sincroniza el flag interno de fullscreen con el estado real del navegador. Debe llamarse antes de diferir handleCanvasResized. No-op fuera de Emscripten + void nextPresentationMode(); // Cicla integer_scale -> letterbox -> stretched -> overscan + void setPresentationMode(Options::PresentationMode mode); // Estableix el mode de presentacio del canvas + [[nodiscard]] static auto getPresentationModeName() -> const char *; // Nom curt del mode actual (per a notificacions) + void toggleVSync(); // Alterna el V-Sync + void setVSync(bool enabled); // Establece el V-Sync + auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió) + auto incWindowZoom() -> bool; // Aumenta el zoom de la ventana (devuelve true si cambió) + auto setWindowZoom(int zoom) -> bool; // Establece el zoom de la ventana (devuelve true si cambió) // Borde void setBorderColor(Color color); // Cambia el color del borde diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index 1cfdf7f..814be2d 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -292,21 +292,48 @@ namespace Rendering { // computeViewport — dimensions lògiques del canvas dins del swapchain (letterbox) // --------------------------------------------------------------------------- auto SDL3GPUShader::computeViewport(Uint32 sw, Uint32 sh) const -> Viewport { + const auto SWF = static_cast(sw); + const auto SHF = static_cast(sh); + const float CANVAS_RATIO = static_cast(game_width_) / static_cast(game_height_); + const float WINDOW_RATIO = SWF / SHF; + float vw = 0.0F; float vh = 0.0F; - if (integer_scale_) { - const int SCALE = std::max(1, std::min(static_cast(sw) / game_width_, static_cast(sh) / game_height_)); - vw = static_cast(game_width_ * SCALE); - vh = static_cast(game_height_ * SCALE); - } else { - const float SCALE = std::min( - static_cast(sw) / static_cast(game_width_), - static_cast(sh) / static_cast(game_height_)); - vw = static_cast(game_width_) * SCALE; - vh = static_cast(game_height_) * SCALE; + switch (presentation_mode_) { + case PresentationMode::INTEGER_SCALE: { + const int SCALE = std::max(1, std::min(static_cast(sw) / game_width_, static_cast(sh) / game_height_)); + vw = static_cast(game_width_ * SCALE); + vh = static_cast(game_height_ * SCALE); + break; + } + case PresentationMode::LETTERBOX: { + if (WINDOW_RATIO >= CANVAS_RATIO) { + vh = SHF; + vw = SHF * CANVAS_RATIO; + } else { + vw = SWF; + vh = SWF / CANVAS_RATIO; + } + break; + } + case PresentationMode::STRETCHED: { + vw = SWF; + vh = SHF; + break; + } + case PresentationMode::OVERSCAN: { + if (WINDOW_RATIO >= CANVAS_RATIO) { + vw = SWF; + vh = SWF / CANVAS_RATIO; + } else { + vh = SHF; + vw = SHF * CANVAS_RATIO; + } + break; + } } - const float VX = std::floor((static_cast(sw) - vw) * 0.5F); - const float VY = std::floor((static_cast(sh) - vh) * 0.5F); + const float VX = std::floor((SWF - vw) * 0.5F); + const float VY = std::floor((SHF - vh) * 0.5F); return {.x = VX, .y = VY, .w = vw, .h = vh}; } @@ -569,8 +596,8 @@ namespace Rendering { } } - void SDL3GPUShader::setScaleMode(bool integer_scale) { - integer_scale_ = integer_scale; + void SDL3GPUShader::setPresentationMode(PresentationMode mode) { + presentation_mode_ = mode; } // --------------------------------------------------------------------------- diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp index bd7e353..be38b6e 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp @@ -97,8 +97,8 @@ namespace Rendering { // Activa/desactiva VSync en el swapchain void setVSync(bool vsync) override; - // Activa/desactiva escalado entero (integer scale) - void setScaleMode(bool integer_scale) override; + // Estableix el mode de presentacio del canvas + void setPresentationMode(PresentationMode mode) override; // Selecciona el shader de post-procesado activo (POSTFX o CRTPI) void setActiveShader(ShaderType type) override; @@ -195,7 +195,7 @@ namespace Rendering { std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige) bool is_initialized_ = false; bool vsync_ = true; - bool integer_scale_ = false; + PresentationMode presentation_mode_ = PresentationMode::INTEGER_SCALE; }; } // namespace Rendering diff --git a/source/core/rendering/shader_backend.hpp b/source/core/rendering/shader_backend.hpp index 7414aaa..6bd48b8 100644 --- a/source/core/rendering/shader_backend.hpp +++ b/source/core/rendering/shader_backend.hpp @@ -112,9 +112,16 @@ namespace Rendering { virtual void setVSync(bool /*vsync*/) {} /** - * @brief Activa o desactiva el escalado entero (integer scale) + * @brief Estableix el mode de presentacio del canvas dins del swapchain. + * El backend calcula el viewport en consequencia. */ - virtual void setScaleMode(bool /*integer_scale*/) {} + enum class PresentationMode : std::uint8_t { + INTEGER_SCALE, + LETTERBOX, + STRETCHED, + OVERSCAN + }; + virtual void setPresentationMode(PresentationMode /*mode*/) {} /** * @brief Verifica si el backend está usando aceleración por hardware diff --git a/source/game/defaults.hpp b/source/game/defaults.hpp index d78bd50..fefd861 100644 --- a/source/game/defaults.hpp +++ b/source/game/defaults.hpp @@ -19,7 +19,8 @@ namespace Defaults::Video { constexpr SDL_ScaleMode SCALE_MODE = SDL_ScaleMode::SDL_SCALEMODE_NEAREST; constexpr bool FULLSCREEN = false; constexpr bool VSYNC = true; - constexpr bool INTEGER_SCALE = true; + // INTEGER_SCALE eliminat: ara es part de PresentationMode (a options.hpp). + // El default es defineix literal alli: PresentationMode::INTEGER_SCALE. constexpr bool GPU_ACCELERATION = true; constexpr const char *GPU_PREFERRED_DRIVER = ""; constexpr bool SHADER_ENABLED = false; diff --git a/source/game/options.cpp b/source/game/options.cpp index 16cc1d2..165c3c2 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -31,6 +31,42 @@ namespace Options { std::string crtpi_file_path; int current_crtpi_preset = 0; + // Conversions PresentationMode <-> string per a config.yaml i UI + auto presentationModeToString(PresentationMode m) -> const char * { + switch (m) { + case PresentationMode::INTEGER_SCALE: + return "integer_scale"; + case PresentationMode::LETTERBOX: + return "letterbox"; + case PresentationMode::STRETCHED: + return "stretched"; + case PresentationMode::OVERSCAN: + return "overscan"; + } + return "integer_scale"; + } + + auto presentationModeFromString(const std::string &s) -> PresentationMode { + if (s == "letterbox") { return PresentationMode::LETTERBOX; } + if (s == "stretched") { return PresentationMode::STRETCHED; } + if (s == "overscan") { return PresentationMode::OVERSCAN; } + return PresentationMode::INTEGER_SCALE; + } + + auto nextPresentationMode(PresentationMode m) -> PresentationMode { + switch (m) { + case PresentationMode::INTEGER_SCALE: + return PresentationMode::LETTERBOX; + case PresentationMode::LETTERBOX: + return PresentationMode::STRETCHED; + case PresentationMode::STRETCHED: + return PresentationMode::OVERSCAN; + case PresentationMode::OVERSCAN: + return PresentationMode::INTEGER_SCALE; + } + return PresentationMode::INTEGER_SCALE; + } + // Lectura tolerant d'un camp YAML: assigna a `target` el valor del camp // si existeix i el tipus encaixa. Si la clau no hi és o el tipus YAML // no és compatible amb T, conserva el valor previ de `target` (default). @@ -82,7 +118,16 @@ namespace Options { parseBoolField(vid, "fullscreen", video.fullscreen); parseBoolField(vid, "vsync", video.vsync); - parseBoolField(vid, "integer_scale", video.integer_scale); + // presentation_mode (nou): prefereix string explicit; cau a integer_scale legacy (bool) si no. + std::string pm_str; + if (tryGet(vid, "presentation_mode", pm_str)) { + video.presentation_mode = presentationModeFromString(pm_str); + } else { + bool legacy_integer_scale = true; + if (tryGet(vid, "integer_scale", legacy_integer_scale)) { + video.presentation_mode = legacy_integer_scale ? PresentationMode::INTEGER_SCALE : PresentationMode::LETTERBOX; + } + } int scale_mode_int = static_cast(video.scale_mode); if (tryGet(vid, "scale_mode", scale_mode_int)) { video.scale_mode = static_cast(scale_mode_int); @@ -197,7 +242,7 @@ namespace Options { // En Emscripten la ventana la gestiona el navegador window.zoom = 4; video.fullscreen = false; - video.integer_scale = true; + video.presentation_mode = PresentationMode::INTEGER_SCALE; #endif // Dispositius d'entrada per defecte @@ -283,7 +328,8 @@ namespace Options { file << "video:\n"; file << " fullscreen: " << boolToString(video.fullscreen) << "\n"; file << " vsync: " << boolToString(video.vsync) << "\n"; - file << " integer_scale: " << boolToString(video.integer_scale) << "\n"; + file << " presentation_mode: " << presentationModeToString(video.presentation_mode) + << " # integer_scale | letterbox | stretched | overscan\n"; file << " scale_mode: " << static_cast(video.scale_mode) << " # " << static_cast(SDL_SCALEMODE_NEAREST) << ": nearest, " << static_cast(SDL_SCALEMODE_LINEAR) << ": linear\n"; diff --git a/source/game/options.hpp b/source/game/options.hpp index bcf0c11..5dd73e7 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -18,6 +18,16 @@ namespace Options { + // Modes de presentacio del canvas virtual a la finestra. Tot fullscreen i + // windowed amb zoom no-fit el respecta; en windowed amb zoom exacte (1x/2x/3x) + // l'efecte es null perque el canvas ja encaixa amb la finestra. + enum class PresentationMode : std::uint8_t { + INTEGER_SCALE, // Multiple enter (1x, 2x, 3x...), centrat, amb barres + LETTERBOX, // Mante aspect ratio, centrat, amb barres + STRETCHED, // Omple tota la finestra, deforma l'aspect ratio + OVERSCAN // Mante aspect ratio i omple la finestra retallant el contingut fora + }; + struct Window { std::string caption = Defaults::Window::CAPTION; int zoom = Defaults::Window::ZOOM; @@ -42,11 +52,16 @@ namespace Options { SDL_ScaleMode scale_mode = Defaults::Video::SCALE_MODE; bool fullscreen = Defaults::Video::FULLSCREEN; bool vsync = Defaults::Video::VSYNC; - bool integer_scale = Defaults::Video::INTEGER_SCALE; + PresentationMode presentation_mode = PresentationMode::INTEGER_SCALE; GPU gpu; ShaderConfig shader; }; + // Conversions string <-> PresentationMode per a config.yaml i notificacions + auto presentationModeToString(PresentationMode m) -> const char *; + auto presentationModeFromString(const std::string &s) -> PresentationMode; + auto nextPresentationMode(PresentationMode m) -> PresentationMode; + struct Music { bool enabled = Defaults::Music::ENABLED; float volume = Defaults::Music::VOLUME;