feat(render): resolució d'offscreen configurable via YAML

Separa el tamany lògic (1280×720) del render target offscreen. Llista
tancada de 5 presets 16:9 (720p/900p/1080p/1440p/2160p) llegida de
rendering.render_{width,height} amb fallback a 1280×720 si invàlida.
Inclou API resizeRenderTarget() preparada per al menú de servei futur.
This commit is contained in:
2026-05-21 08:46:22 +02:00
parent 4252f3327f
commit 5d1dae1d86
6 changed files with 150 additions and 17 deletions
+7
View File
@@ -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 {
+27
View File
@@ -3,9 +3,36 @@
#pragma once
#include <algorithm>
#include <array>
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<ResolutionPreset, 5> 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
@@ -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<uint32_t>(logical_w_);
tex_info.height = static_cast<uint32_t>(logical_h_);
tex_info.width = static_cast<uint32_t>(render_w_);
tex_info.height = static_cast<uint32_t>(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;
@@ -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};
+38 -5
View File
@@ -10,7 +10,9 @@
#include <iostream>
#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<float>(Defaults::Game::WIDTH),
static_cast<float>(Defaults::Game::HEIGHT))) {
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;
@@ -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]";
}
+29 -1
View File
@@ -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);