diff --git a/source/core/config/engine_config.hpp b/source/core/config/engine_config.hpp index 3890dfe..72ae20d 100644 --- a/source/core/config/engine_config.hpp +++ b/source/core/config/engine_config.hpp @@ -27,6 +27,13 @@ namespace Config { struct RenderingConfig { int vsync{1}; // 0=disabled, 1=enabled int antialias{1}; // 0=disabled, 1=enabled (AA geomètric a les línies, toggle F5) + // Resolució del render target offscreen (independent del tamany lògic + // 1280×720 del joc). Aquesta és la resolució real on rasteritzen les + // línies abans de l'escala final a la swapchain; pujar-la millora + // la nitidesa en finestres grans i fullscreen. Llista tancada de + // presets 16:9 — veure Defaults::Rendering::RESOLUTION_PRESETS. + int render_width{1280}; + int render_height{720}; }; struct KeyboardBindings { diff --git a/source/core/defaults/rendering.hpp b/source/core/defaults/rendering.hpp index 819b1fd..0480516 100644 --- a/source/core/defaults/rendering.hpp +++ b/source/core/defaults/rendering.hpp @@ -3,9 +3,36 @@ #pragma once +#include +#include + namespace Defaults::Rendering { constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled constexpr int ANTIALIAS_DEFAULT = 1; // 0=disabled, 1=enabled (AA geomètric a les línies) + // Resolució del render target offscreen. El tamany lògic del joc roman a + // 1280×720 (coordenades dels objectes); aquesta és la resolució física a + // la qual es rasteritzen les línies abans de la composició final. + struct ResolutionPreset { + int w; + int h; + }; + + constexpr std::array RESOLUTION_PRESETS{{ + {.w = 1280, .h = 720}, // HD 720p (default) + {.w = 1600, .h = 900}, // HD+ 900p + {.w = 1920, .h = 1080}, // Full HD 1080p + {.w = 2560, .h = 1440}, // QHD 1440p + {.w = 3840, .h = 2160} // 4K UHD 2160p + }}; + + constexpr int RENDER_WIDTH_DEFAULT = 1280; + constexpr int RENDER_HEIGHT_DEFAULT = 720; + + constexpr auto isValidRenderResolution(int w, int h) -> bool { + return std::ranges::any_of(RESOLUTION_PRESETS, + [w, h](const ResolutionPreset& preset) { return preset.w == w && preset.h == h; }); + } + } // namespace Defaults::Rendering diff --git a/source/core/rendering/gpu/gpu_frame_renderer.cpp b/source/core/rendering/gpu/gpu_frame_renderer.cpp index bdf6fc9..1a3bb78 100644 --- a/source/core/rendering/gpu/gpu_frame_renderer.cpp +++ b/source/core/rendering/gpu/gpu_frame_renderer.cpp @@ -14,9 +14,11 @@ namespace Rendering::GPU { GpuFrameRenderer::~GpuFrameRenderer() { destroy(); } - auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h) -> bool { + auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h, float render_w, float render_h) -> bool { logical_w_ = logical_w; logical_h_ = logical_h; + render_w_ = render_w; + render_h_ = render_h; if (!device_.init(window)) { return false; @@ -47,13 +49,15 @@ namespace Rendering::GPU { return false; } - // Textura offscreen del tamaño lógico del juego, COLOR_TARGET + SAMPLER. + // Textura offscreen del tamaño físico de render, COLOR_TARGET + SAMPLER. + // El tamaño lógico se aplica a los vértices vía UBO; el offscreen puede + // ser de mayor resolución para ganar nitidez tras el upscale a la swapchain. SDL_GPUTextureCreateInfo tex_info{}; tex_info.type = SDL_GPU_TEXTURETYPE_2D; tex_info.format = offscreen_format_; tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; - tex_info.width = static_cast(logical_w_); - tex_info.height = static_cast(logical_h_); + tex_info.width = static_cast(render_w_); + tex_info.height = static_cast(render_h_); tex_info.layer_count_or_depth = 1; tex_info.num_levels = 1; tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1; @@ -107,6 +111,19 @@ namespace Rendering::GPU { indices_.clear(); } + auto GpuFrameRenderer::resizeRenderTarget(float render_w, float render_h) -> bool { + // Solo seguro fuera de un frame: si el cmd buffer está vivo y referencia + // la textura antigua, recrearla provocaría un cuelgue/UB. + if (isInsideFrame()) { + std::cerr << "[GpuFrameRenderer] resizeRenderTarget llamado dentro de frame, ignorado\n"; + return false; + } + destroyOffscreen(); + render_w_ = render_w; + render_h_ = render_h; + return createOffscreen(); + } + auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) -> bool { // Los clear_* se ignoran: el fondo lo pinta el postpro. Mantenemos la // firma para no romper el SDLManager. @@ -438,8 +455,9 @@ namespace Rendering::GPU { ubo.background_max_g = BG_MAX_G; ubo.background_max_b = BG_MAX_B; ubo.background_max_a = 1.0F; - ubo.texel_size_x = 1.0F / logical_w_; - ubo.texel_size_y = 1.0F / logical_h_; + // El sampling del bloom muestrea el offscreen → texel size del tamaño físico. + ubo.texel_size_x = 1.0F / render_w_; + ubo.texel_size_y = 1.0F / render_h_; ubo.pad_b = 0.0F; ubo.pad_c = 0.0F; diff --git a/source/core/rendering/gpu/gpu_frame_renderer.hpp b/source/core/rendering/gpu/gpu_frame_renderer.hpp index 0428e7e..fad67e3 100644 --- a/source/core/rendering/gpu/gpu_frame_renderer.hpp +++ b/source/core/rendering/gpu/gpu_frame_renderer.hpp @@ -59,12 +59,24 @@ namespace Rendering::GPU { GpuFrameRenderer(GpuFrameRenderer&&) = delete; auto operator=(GpuFrameRenderer&&) -> GpuFrameRenderer& = delete; - // Crea device + pipeline + offscreen + sampler. logical_w/h = tamaño - // en píxeles lógicos del juego (1280×720), usado como base del - // offscreen y de la transformación a NDC del shader de líneas. - [[nodiscard]] auto init(SDL_Window* window, float logical_w, float logical_h) -> bool; + // Crea device + pipeline + offscreen + sampler. + // logical_w/h: tamaño en píxeles lógicos del juego (1280×720). Lo + // consume el shader de líneas para transformar a NDC. + // render_w/h: tamaño físico del offscreen donde se rasterizan las + // líneas. Puede ser > logical para ganar nitidez al + // escalar a la swapchain (configurable vía YAML). + [[nodiscard]] auto init(SDL_Window* window, + float logical_w, + float logical_h, + float render_w, + float render_h) -> bool; void destroy(); + // Recrea el offscreen con un nuevo tamaño físico de render. Solo es + // seguro fuera de un frame (isInsideFrame() == false). Devuelve false + // si está dentro de frame o si la creación de la textura falla. + [[nodiscard]] auto resizeRenderTarget(float render_w, float render_h) -> bool; + // beginFrame: adquiere swapchain, abre render pass sobre offscreen // con clear a negro. Devuelve false si no hay textura disponible. // Los argumentos clear_r/g/b se ignoran (compatibilidad de API: el @@ -114,10 +126,18 @@ namespace Rendering::GPU { GpuLinePipeline line_pipeline_; GpuPostFxPipeline postfx_pipeline_; - // Tamaño lógico del juego (= tamaño del offscreen). + // Tamaño lógico del juego: espacio de coordenadas de las primitivas + // (vértices, UBO del line shader). Fijo a 1280×720. float logical_w_{1280.0F}; float logical_h_{720.0F}; + // Tamaño físico del offscreen (configurable). Independiente del lógico: + // las coordenadas en NDC son agnósticas a la resolución de salida, así + // que rasterizar a mayor render_w_/h_ da líneas más nítidas tras el + // upscale lineal a la swapchain. + float render_w_{1280.0F}; + float render_h_{720.0F}; + // Viewport del pase final en píxeles físicos. <0 = full window. float viewport_x_{0.0F}; float viewport_y_{0.0F}; diff --git a/source/core/rendering/sdl_manager.cpp b/source/core/rendering/sdl_manager.cpp index ffecacb..a672812 100644 --- a/source/core/rendering/sdl_manager.cpp +++ b/source/core/rendering/sdl_manager.cpp @@ -10,7 +10,9 @@ #include #include "core/config/postfx_config.hpp" -#include "core/defaults.hpp" +#include "core/defaults/game.hpp" +#include "core/defaults/rendering.hpp" +#include "core/defaults/window.hpp" #include "core/input/mouse.hpp" #include "core/rendering/coordinate_transform.hpp" #include "core/system/notifier.hpp" @@ -22,7 +24,9 @@ namespace { int width, int height, bool fullscreen, - int initial_vsync) -> bool { + 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", @@ -44,9 +48,13 @@ namespace { } // 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(Defaults::Game::WIDTH), - static_cast(Defaults::Game::HEIGHT))) { + static_cast(Defaults::Game::HEIGHT), + static_cast(render_width), + static_cast(render_height))) { std::cerr << "Error inicialitzant GpuFrameRenderer\n"; SDL_DestroyWindow(window); return false; @@ -80,7 +88,30 @@ SDLManager::SDLManager(int width, int height, bool fullscreen, Config::EngineCon calculateMaxWindowSize(); - if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, is_fullscreen_, cfg_->rendering.vsync)) { + // 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; } @@ -97,7 +128,9 @@ SDLManager::SDLManager(int width, int height, bool fullscreen, Config::EngineCon std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_ << " (logic: " << Defaults::Game::WIDTH << "x" - << Defaults::Game::HEIGHT << ")"; + << Defaults::Game::HEIGHT + << ", render: " << effective_render_w << "x" << effective_render_h + << ")"; if (is_fullscreen_) { std::cout << " [FULLSCREEN]"; } diff --git a/source/game/config_yaml.cpp b/source/game/config_yaml.cpp index 18d9f6b..7f215b1 100644 --- a/source/game/config_yaml.cpp +++ b/source/game/config_yaml.cpp @@ -204,6 +204,8 @@ namespace ConfigYaml { // Rendering rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT; + rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT; + rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT; // Version version = std::string(Project::VERSION); @@ -275,6 +277,28 @@ namespace ConfigYaml { rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT; } } + + // Resolució de render: validem el parell (w, h) contra la llista + // tancada de presets 16:9. Si falla l'una o l'altra, fem fallback + // dels dos camps al default per mantenir un parell vàlid. + int candidate_w = rendering.render_width; + int candidate_h = rendering.render_height; + readField(rend, "render_width", candidate_w, Defaults::Rendering::RENDER_WIDTH_DEFAULT); + readField(rend, "render_height", candidate_h, Defaults::Rendering::RENDER_HEIGHT_DEFAULT); + if (Defaults::Rendering::isValidRenderResolution(candidate_w, candidate_h)) { + rendering.render_width = candidate_w; + rendering.render_height = candidate_h; + } else { + if (console) { + std::cerr << "Resolució de render invàlida al YAML (" + << candidate_w << "x" << candidate_h + << "), fallback a " + << Defaults::Rendering::RENDER_WIDTH_DEFAULT << "x" + << Defaults::Rendering::RENDER_HEIGHT_DEFAULT << '\n'; + } + rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT; + rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT; + } } } @@ -501,7 +525,11 @@ namespace ConfigYaml { file << "# RENDERITZACIÓ\n"; file << "rendering:\n"; - file << " vsync: " << rendering.vsync << " # 0=disabled, 1=enabled\n\n"; + file << " vsync: " << rendering.vsync << " # 0=disabled, 1=enabled\n"; + file << " render_width: " << rendering.render_width + << " # Presets 16:9: 1280, 1600, 1920, 2560, 3840 (fallback 1280)\n"; + file << " render_height: " << rendering.render_height + << " # Parell amb render_width (720, 900, 1080, 1440, 2160)\n\n"; // Guardar controls de jugadors savePlayer1ControlsToYaml(file);