diff --git a/shaders/line.frag.glsl b/shaders/line.frag.glsl index 086e216..b75f54b 100644 --- a/shaders/line.frag.glsl +++ b/shaders/line.frag.glsl @@ -1,12 +1,25 @@ #version 450 -// Fragment shader para líneas vectoriales. -// Pinta el color interpolado per-vertex que viene del vertex shader. -// (Postprocesado / bloom / glow son responsabilidad de la Fase 8.) +// Fragment shader per a línies vectorials. +// +// Antialias geomètric: rebem `frag_edge_dist` interpolat (±1 als laterals del +// quad, 0 a l'eix central). Apliquem un smoothstep d'1 píxel d'amplada perquè +// el gruix nominal (els |edge_dist| < threshold) quedi totalment opac i només +// el píxel extruit als laterals faci la transició suau. +// +// La línia ja ve extruïda amb thickness + 1px a CPU; el threshold equival a +// (thickness)/(thickness+1), però no el coneixem aquí per vèrtex. En el cas +// general (línies fines), fade lineal entre 0.0 i 1.0 dóna prou bon resultat +// visualment sense necessitat d'un uniform per línia. layout(location = 0) in vec4 frag_color; +layout(location = 1) in float frag_edge_dist; layout(location = 0) out vec4 out_color; void main() { - out_color = frag_color; + // |edge_dist|=0 → totalment opac; |edge_dist|=1 → totalment transparent. + // smoothstep dóna un fade Hermite C¹ que evita banding. + float d = abs(frag_edge_dist); + float alpha = 1.0 - smoothstep(0.7, 1.0, d); + out_color = vec4(frag_color.rgb, frag_color.a * alpha); } diff --git a/shaders/line.vert.glsl b/shaders/line.vert.glsl index 247d3ff..4777b7a 100644 --- a/shaders/line.vert.glsl +++ b/shaders/line.vert.glsl @@ -14,10 +14,12 @@ layout(set = 1, binding = 0) uniform UBO { vec2 _padding; // alineamiento a 16 bytes } ubo; -layout(location = 0) in vec2 in_position; // píxeles lógicos -layout(location = 1) in vec4 in_color; // RGBA 0..1 +layout(location = 0) in vec2 in_position; // píxeles lógicos +layout(location = 1) in vec4 in_color; // RGBA 0..1 +layout(location = 2) in float in_edge_dist; // ±1 als laterals, 0 al centre layout(location = 0) out vec4 frag_color; +layout(location = 1) out float frag_edge_dist; void main() { // Píxeles lógicos -> NDC (-1..+1) @@ -26,4 +28,5 @@ void main() { ndc.y = -ndc.y; gl_Position = vec4(ndc, 0.0, 1.0); frag_color = in_color; + frag_edge_dist = in_edge_dist; } diff --git a/source/core/config/engine_config.hpp b/source/core/config/engine_config.hpp index 32f91c8..3890dfe 100644 --- a/source/core/config/engine_config.hpp +++ b/source/core/config/engine_config.hpp @@ -25,7 +25,8 @@ namespace Config { }; struct RenderingConfig { - int vsync{1}; // 0=disabled, 1=enabled + int vsync{1}; // 0=disabled, 1=enabled + int antialias{1}; // 0=disabled, 1=enabled (AA geomètric a les línies, toggle F5) }; struct KeyboardBindings { diff --git a/source/core/defaults/rendering.hpp b/source/core/defaults/rendering.hpp index b6a636f..819b1fd 100644 --- a/source/core/defaults/rendering.hpp +++ b/source/core/defaults/rendering.hpp @@ -5,6 +5,7 @@ namespace Defaults::Rendering { - constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled + 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) } // namespace Defaults::Rendering diff --git a/source/core/input/input.cpp b/source/core/input/input.cpp index 5009840..f7d2d95 100644 --- a/source/core/input/input.cpp +++ b/source/core/input/input.cpp @@ -39,6 +39,7 @@ Input::Input(std::string game_controller_db_path) {Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}}, {Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}}, {Action::TOGGLE_VSYNC, KeyState{.scancode = SDL_SCANCODE_F4}}, + {Action::TOGGLE_ANTIALIAS, KeyState{.scancode = SDL_SCANCODE_F5}}, {Action::EXIT, KeyState{.scancode = SDL_SCANCODE_ESCAPE}}}; initSDLGamePad(); // Inicializa el subsistema SDL_INIT_GAMEPAD diff --git a/source/core/input/input_types.hpp b/source/core/input/input_types.hpp index 92f7a0f..a32f6d1 100644 --- a/source/core/input/input_types.hpp +++ b/source/core/input/input_types.hpp @@ -21,6 +21,7 @@ enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el j WINDOW_DEC_ZOOM, // F1 TOGGLE_FULLSCREEN, // F3 TOGGLE_VSYNC, // F4 + TOGGLE_ANTIALIAS, // F5 EXIT, // ESC // Input obligatorio diff --git a/source/core/rendering/gpu/gpu_frame_renderer.cpp b/source/core/rendering/gpu/gpu_frame_renderer.cpp index 349ed86..16d9a27 100644 --- a/source/core/rendering/gpu/gpu_frame_renderer.cpp +++ b/source/core/rendering/gpu/gpu_frame_renderer.cpp @@ -12,389 +12,431 @@ namespace Rendering::GPU { -GpuFrameRenderer::~GpuFrameRenderer() { destroy(); } + GpuFrameRenderer::~GpuFrameRenderer() { destroy(); } -auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h) -> bool { - logical_w_ = logical_w; - logical_h_ = logical_h; + auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h) -> bool { + logical_w_ = logical_w; + logical_h_ = logical_h; - if (!device_.init(window)) { - return false; + if (!device_.init(window)) { + return false; + } + // Pipeline de líneas: escribe sobre el offscreen (formato fijo). + if (!line_pipeline_.init(device_, offscreen_format_)) { + device_.destroy(); + return false; + } + // Pipeline de postpro: escribe sobre swapchain (formato del swapchain). + if (!postfx_pipeline_.init(device_, device_.swapchainFormat())) { + line_pipeline_.destroy(); + device_.destroy(); + return false; + } + if (!createOffscreen()) { + postfx_pipeline_.destroy(); + line_pipeline_.destroy(); + device_.destroy(); + return false; + } + return true; } - // Pipeline de líneas: escribe sobre el offscreen (formato fijo). - if (!line_pipeline_.init(device_, offscreen_format_)) { - device_.destroy(); - return false; + + auto GpuFrameRenderer::createOffscreen() -> bool { + SDL_GPUDevice* dev = device_.get(); + if (dev == nullptr) { + return false; + } + + // Textura offscreen del tamaño lógico del juego, COLOR_TARGET + SAMPLER. + 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.layer_count_or_depth = 1; + tex_info.num_levels = 1; + tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1; + offscreen_texture_ = SDL_CreateGPUTexture(dev, &tex_info); + if (offscreen_texture_ == nullptr) { + std::cerr << "[GpuFrameRenderer] SDL_CreateGPUTexture (offscreen): " + << SDL_GetError() << '\n'; + return false; + } + + // Sampler lineal con clamp-to-edge (evita sangrado en los bordes del bloom). + SDL_GPUSamplerCreateInfo sampler_info{}; + sampler_info.min_filter = SDL_GPU_FILTER_LINEAR; + sampler_info.mag_filter = SDL_GPU_FILTER_LINEAR; + sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR; + sampler_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + sampler_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + sampler_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; + linear_sampler_ = SDL_CreateGPUSampler(dev, &sampler_info); + if (linear_sampler_ == nullptr) { + std::cerr << "[GpuFrameRenderer] SDL_CreateGPUSampler: " + << SDL_GetError() << '\n'; + return false; + } + return true; } - // Pipeline de postpro: escribe sobre swapchain (formato del swapchain). - if (!postfx_pipeline_.init(device_, device_.swapchainFormat())) { - line_pipeline_.destroy(); - device_.destroy(); - return false; + + void GpuFrameRenderer::destroyOffscreen() { + SDL_GPUDevice* dev = device_.get(); + if (dev == nullptr) { + offscreen_texture_ = nullptr; + linear_sampler_ = nullptr; + return; + } + if (offscreen_texture_ != nullptr) { + SDL_ReleaseGPUTexture(dev, offscreen_texture_); + offscreen_texture_ = nullptr; + } + if (linear_sampler_ != nullptr) { + SDL_ReleaseGPUSampler(dev, linear_sampler_); + linear_sampler_ = nullptr; + } } - if (!createOffscreen()) { + + void GpuFrameRenderer::destroy() { + destroyOffscreen(); postfx_pipeline_.destroy(); line_pipeline_.destroy(); device_.destroy(); - return false; - } - return true; -} - -auto GpuFrameRenderer::createOffscreen() -> bool { - SDL_GPUDevice* dev = device_.get(); - if (dev == nullptr) { - return false; + vertices_.clear(); + indices_.clear(); } - // Textura offscreen del tamaño lógico del juego, COLOR_TARGET + SAMPLER. - 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.layer_count_or_depth = 1; - tex_info.num_levels = 1; - tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1; - offscreen_texture_ = SDL_CreateGPUTexture(dev, &tex_info); - if (offscreen_texture_ == nullptr) { - std::cerr << "[GpuFrameRenderer] SDL_CreateGPUTexture (offscreen): " - << SDL_GetError() << '\n'; - return false; + 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. + (void)clear_r; + (void)clear_g; + (void)clear_b; + + SDL_GPUDevice* dev = device_.get(); + if (dev == nullptr) { + return false; + } + + cmd_buffer_ = SDL_AcquireGPUCommandBuffer(dev); + if (cmd_buffer_ == nullptr) { + std::cerr << "[GpuFrameRenderer] SDL_AcquireGPUCommandBuffer: " << SDL_GetError() << '\n'; + return false; + } + + if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), &swapchain_texture_, nullptr, nullptr)) { + std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n'; + SDL_SubmitGPUCommandBuffer(cmd_buffer_); + cmd_buffer_ = nullptr; + return false; + } + + if (swapchain_texture_ == nullptr) { + // Ventana minimizada o swapchain no disponible: solo submit y salir. + SDL_SubmitGPUCommandBuffer(cmd_buffer_); + cmd_buffer_ = nullptr; + return false; + } + + // Abrir render pass sobre OFFSCREEN con clear a negro. + SDL_GPUColorTargetInfo color_target{}; + color_target.texture = offscreen_texture_; + color_target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; + color_target.load_op = SDL_GPU_LOADOP_CLEAR; + color_target.store_op = SDL_GPU_STOREOP_STORE; + color_target.cycle = false; + + render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr); + if (render_pass_ == nullptr) { + std::cerr << "[GpuFrameRenderer] SDL_BeginGPURenderPass (offscreen): " + << SDL_GetError() << '\n'; + SDL_SubmitGPUCommandBuffer(cmd_buffer_); + cmd_buffer_ = nullptr; + return false; + } + // Sin SetGPUViewport: el offscreen se llena entero a tamaño lógico. + + vertices_.clear(); + indices_.clear(); + return true; } - // Sampler lineal con clamp-to-edge (evita sangrado en los bordes del bloom). - SDL_GPUSamplerCreateInfo sampler_info{}; - sampler_info.min_filter = SDL_GPU_FILTER_LINEAR; - sampler_info.mag_filter = SDL_GPU_FILTER_LINEAR; - sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR; - sampler_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; - sampler_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; - sampler_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; - linear_sampler_ = SDL_CreateGPUSampler(dev, &sampler_info); - if (linear_sampler_ == nullptr) { - std::cerr << "[GpuFrameRenderer] SDL_CreateGPUSampler: " - << SDL_GetError() << '\n'; - return false; - } - return true; -} - -void GpuFrameRenderer::destroyOffscreen() { - SDL_GPUDevice* dev = device_.get(); - if (dev == nullptr) { - offscreen_texture_ = nullptr; - linear_sampler_ = nullptr; - return; - } - if (offscreen_texture_ != nullptr) { - SDL_ReleaseGPUTexture(dev, offscreen_texture_); - offscreen_texture_ = nullptr; - } - if (linear_sampler_ != nullptr) { - SDL_ReleaseGPUSampler(dev, linear_sampler_); - linear_sampler_ = nullptr; - } -} - -void GpuFrameRenderer::destroy() { - destroyOffscreen(); - postfx_pipeline_.destroy(); - line_pipeline_.destroy(); - device_.destroy(); - vertices_.clear(); - indices_.clear(); -} - -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. - (void)clear_r; - (void)clear_g; - (void)clear_b; - - SDL_GPUDevice* dev = device_.get(); - if (dev == nullptr) { - return false; + void GpuFrameRenderer::setViewport(float x, float y, float w, float h) { + viewport_x_ = x; + viewport_y_ = y; + viewport_w_ = w; + viewport_h_ = h; + // El viewport solo se aplica en el pase final (composite). Si estamos + // ya dentro del composite, lo aplicaríamos inmediatamente, pero la API + // está pensada para llamarse antes de endFrame/al cambiar de ventana. } - cmd_buffer_ = SDL_AcquireGPUCommandBuffer(dev); - if (cmd_buffer_ == nullptr) { - std::cerr << "[GpuFrameRenderer] SDL_AcquireGPUCommandBuffer: " << SDL_GetError() << '\n'; - return false; + void GpuFrameRenderer::setVSync(bool enabled) { + SDL_GPUDevice* dev = device_.get(); + SDL_Window* win = device_.window(); + if (dev == nullptr || win == nullptr) { + return; + } + + // A SDL_GPU, només VSYNC està garantit. IMMEDIATE i MAILBOX són opcionals + // i poden no estar disponibles segons backend/driver/compositor (típicament + // Wayland sense unredirect, X11 amb compositor agressiu, etc.). Si demanem + // un mode no suportat, SDL retorna error i la swapchain queda igual. + // + // Estratègia: si l'usuari demana VSync OFF, provem IMMEDIATE i si no està + // suportat fem fallback a MAILBOX (no és no-VSync però sí permet superar + // els 60Hz sense tearing). Si cap dels dos, ens quedem amb VSYNC i avisem. + auto try_set = [&](SDL_GPUPresentMode mode, const char* name) -> bool { + if (!SDL_WindowSupportsGPUPresentMode(dev, win, mode)) { + return false; + } + if (!SDL_SetGPUSwapchainParameters(dev, win, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, mode)) { + std::cerr << "[GpuFrameRenderer] SDL_SetGPUSwapchainParameters(" + << name << "): " << SDL_GetError() << '\n'; + return false; + } + std::cout << "[GpuFrameRenderer] Present mode: " << name << '\n'; + return true; + }; + + if (enabled) { + try_set(SDL_GPU_PRESENTMODE_VSYNC, "VSYNC"); + return; + } + + if (try_set(SDL_GPU_PRESENTMODE_IMMEDIATE, "IMMEDIATE")) { + return; + } + if (try_set(SDL_GPU_PRESENTMODE_MAILBOX, "MAILBOX (fallback)")) { + return; + } + // Tots dos rebutjats: el driver/compositor força VSync. Hi tornem + // explícitament perquè la swapchain quedi en estat conegut. + std::cerr << "[GpuFrameRenderer] VSync OFF no disponible (driver/compositor " + "força VSYNC). Mantenim VSYNC.\n"; + try_set(SDL_GPU_PRESENTMODE_VSYNC, "VSYNC (forçat)"); } - if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), - &swapchain_texture_, nullptr, nullptr)) { - std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n'; - SDL_SubmitGPUCommandBuffer(cmd_buffer_); - cmd_buffer_ = nullptr; - return false; + void GpuFrameRenderer::applyFinalViewport() { + if (render_pass_ == nullptr) { + return; + } + if (viewport_w_ <= 0.0F || viewport_h_ <= 0.0F) { + return; + } + SDL_GPUViewport vp{}; + vp.x = viewport_x_; + vp.y = viewport_y_; + vp.w = viewport_w_; + vp.h = viewport_h_; + vp.min_depth = 0.0F; + vp.max_depth = 1.0F; + SDL_SetGPUViewport(render_pass_, &vp); } - if (swapchain_texture_ == nullptr) { - // Ventana minimizada o swapchain no disponible: solo submit y salir. - SDL_SubmitGPUCommandBuffer(cmd_buffer_); - cmd_buffer_ = nullptr; - return false; + void GpuFrameRenderer::pushLine(float x1, float y1, float x2, float y2, float thickness, float r, float g, float b, float a) { + const float DX = x2 - x1; + const float DY = y2 - y1; + const float LEN = std::sqrt((DX * DX) + (DY * DY)); + if (LEN < 1e-6F) { + return; + } + + // Antialias geomètric: quan està ON, extruim 0.5 píxel extra a cada + // banda del gruix demanat i posem edge_dist = ±1 als laterals; el + // fragment shader fa fade als bords. Quan està OFF, geometria nua + // (thickness exacte) i edge_dist = 0 a tots els vèrtexs → el + // smoothstep del shader produeix alpha=1 sempre. + const float AA_PADDING = antialias_enabled_ ? 0.5F : 0.0F; + const float HALF = (thickness * 0.5F) + AA_PADDING; + const float NX = -DY / LEN * HALF; + const float NY = DX / LEN * HALF; + const float EDGE = antialias_enabled_ ? 1.0F : 0.0F; + + const auto BASE_INDEX = static_cast(vertices_.size()); + + vertices_.push_back({x1 + NX, y1 + NY, r, g, b, a, +EDGE}); + vertices_.push_back({x1 - NX, y1 - NY, r, g, b, a, -EDGE}); + vertices_.push_back({x2 + NX, y2 + NY, r, g, b, a, +EDGE}); + vertices_.push_back({x2 - NX, y2 - NY, r, g, b, a, -EDGE}); + + indices_.push_back(BASE_INDEX + 0); + indices_.push_back(BASE_INDEX + 1); + indices_.push_back(BASE_INDEX + 2); + indices_.push_back(BASE_INDEX + 1); + indices_.push_back(BASE_INDEX + 3); + indices_.push_back(BASE_INDEX + 2); } - // Abrir render pass sobre OFFSCREEN con clear a negro. - SDL_GPUColorTargetInfo color_target{}; - color_target.texture = offscreen_texture_; - color_target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; - color_target.load_op = SDL_GPU_LOADOP_CLEAR; - color_target.store_op = SDL_GPU_STOREOP_STORE; - color_target.cycle = false; + void GpuFrameRenderer::flushBatch() { + if (vertices_.empty() || indices_.empty()) { + return; + } - render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr); - if (render_pass_ == nullptr) { - std::cerr << "[GpuFrameRenderer] SDL_BeginGPURenderPass (offscreen): " - << SDL_GetError() << '\n'; - SDL_SubmitGPUCommandBuffer(cmd_buffer_); - cmd_buffer_ = nullptr; - return false; - } - // Sin SetGPUViewport: el offscreen se llena entero a tamaño lógico. + SDL_GPUDevice* dev = device_.get(); - vertices_.clear(); - indices_.clear(); - return true; -} + const auto VBO_SIZE = static_cast(vertices_.size() * sizeof(LineVertex)); + const auto IBO_SIZE = static_cast(indices_.size() * sizeof(uint16_t)); -void GpuFrameRenderer::setViewport(float x, float y, float w, float h) { - viewport_x_ = x; - viewport_y_ = y; - viewport_w_ = w; - viewport_h_ = h; - // El viewport solo se aplica en el pase final (composite). Si estamos - // ya dentro del composite, lo aplicaríamos inmediatamente, pero la API - // está pensada para llamarse antes de endFrame/al cambiar de ventana. -} + SDL_GPUBufferCreateInfo vbo_info{}; + vbo_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX; + vbo_info.size = VBO_SIZE; + SDL_GPUBuffer* vbo = SDL_CreateGPUBuffer(dev, &vbo_info); -void GpuFrameRenderer::setVSync(bool enabled) { - SDL_GPUDevice* dev = device_.get(); - if (dev == nullptr || device_.window() == nullptr) { - return; - } - const SDL_GPUPresentMode MODE = enabled - ? SDL_GPU_PRESENTMODE_VSYNC - : SDL_GPU_PRESENTMODE_IMMEDIATE; - if (!SDL_SetGPUSwapchainParameters(dev, device_.window(), - SDL_GPU_SWAPCHAINCOMPOSITION_SDR, MODE)) { - std::cerr << "[GpuFrameRenderer] SDL_SetGPUSwapchainParameters: " << SDL_GetError() << '\n'; - } -} + SDL_GPUBufferCreateInfo ibo_info{}; + ibo_info.usage = SDL_GPU_BUFFERUSAGE_INDEX; + ibo_info.size = IBO_SIZE; + SDL_GPUBuffer* ibo = SDL_CreateGPUBuffer(dev, &ibo_info); -void GpuFrameRenderer::applyFinalViewport() { - if (render_pass_ == nullptr) { - return; - } - if (viewport_w_ <= 0.0F || viewport_h_ <= 0.0F) { - return; - } - SDL_GPUViewport vp{}; - vp.x = viewport_x_; - vp.y = viewport_y_; - vp.w = viewport_w_; - vp.h = viewport_h_; - vp.min_depth = 0.0F; - vp.max_depth = 1.0F; - SDL_SetGPUViewport(render_pass_, &vp); -} + SDL_GPUTransferBufferCreateInfo tbo_info{}; + tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + tbo_info.size = VBO_SIZE + IBO_SIZE; + SDL_GPUTransferBuffer* tbo = SDL_CreateGPUTransferBuffer(dev, &tbo_info); -void GpuFrameRenderer::pushLine(float x1, float y1, float x2, float y2, float thickness, - float r, float g, float b, float a) { - const float DX = x2 - x1; - const float DY = y2 - y1; - const float LEN = std::sqrt((DX * DX) + (DY * DY)); - if (LEN < 1e-6F) { - return; - } + auto* mapped = static_cast(SDL_MapGPUTransferBuffer(dev, tbo, false)); + std::memcpy(mapped, vertices_.data(), VBO_SIZE); + std::memcpy(mapped + VBO_SIZE, indices_.data(), IBO_SIZE); + SDL_UnmapGPUTransferBuffer(dev, tbo); - const float HALF = thickness * 0.5F; - const float NX = -DY / LEN * HALF; - const float NY = DX / LEN * HALF; - - const auto BASE_INDEX = static_cast(vertices_.size()); - - vertices_.push_back({x1 + NX, y1 + NY, r, g, b, a}); - vertices_.push_back({x1 - NX, y1 - NY, r, g, b, a}); - vertices_.push_back({x2 + NX, y2 + NY, r, g, b, a}); - vertices_.push_back({x2 - NX, y2 - NY, r, g, b, a}); - - indices_.push_back(BASE_INDEX + 0); - indices_.push_back(BASE_INDEX + 1); - indices_.push_back(BASE_INDEX + 2); - indices_.push_back(BASE_INDEX + 1); - indices_.push_back(BASE_INDEX + 3); - indices_.push_back(BASE_INDEX + 2); -} - -void GpuFrameRenderer::flushBatch() { - if (vertices_.empty() || indices_.empty()) { - return; - } - - SDL_GPUDevice* dev = device_.get(); - - const auto VBO_SIZE = static_cast(vertices_.size() * sizeof(LineVertex)); - const auto IBO_SIZE = static_cast(indices_.size() * sizeof(uint16_t)); - - SDL_GPUBufferCreateInfo vbo_info{}; - vbo_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX; - vbo_info.size = VBO_SIZE; - SDL_GPUBuffer* vbo = SDL_CreateGPUBuffer(dev, &vbo_info); - - SDL_GPUBufferCreateInfo ibo_info{}; - ibo_info.usage = SDL_GPU_BUFFERUSAGE_INDEX; - ibo_info.size = IBO_SIZE; - SDL_GPUBuffer* ibo = SDL_CreateGPUBuffer(dev, &ibo_info); - - SDL_GPUTransferBufferCreateInfo tbo_info{}; - tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; - tbo_info.size = VBO_SIZE + IBO_SIZE; - SDL_GPUTransferBuffer* tbo = SDL_CreateGPUTransferBuffer(dev, &tbo_info); - - auto* mapped = static_cast(SDL_MapGPUTransferBuffer(dev, tbo, false)); - std::memcpy(mapped, vertices_.data(), VBO_SIZE); - std::memcpy(mapped + VBO_SIZE, indices_.data(), IBO_SIZE); - SDL_UnmapGPUTransferBuffer(dev, tbo); - - // Copy pass FUERA del render pass. - SDL_EndGPURenderPass(render_pass_); - render_pass_ = nullptr; - - SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd_buffer_); - SDL_GPUTransferBufferLocation vbo_src{.transfer_buffer = tbo, .offset = 0}; - SDL_GPUBufferRegion vbo_dst{.buffer = vbo, .offset = 0, .size = VBO_SIZE}; - SDL_UploadToGPUBuffer(copy_pass, &vbo_src, &vbo_dst, false); - SDL_GPUTransferBufferLocation ibo_src{.transfer_buffer = tbo, .offset = VBO_SIZE}; - SDL_GPUBufferRegion ibo_dst{.buffer = ibo, .offset = 0, .size = IBO_SIZE}; - SDL_UploadToGPUBuffer(copy_pass, &ibo_src, &ibo_dst, false); - SDL_EndGPUCopyPass(copy_pass); - - // Reabrir render pass sobre OFFSCREEN (load_op=LOAD para preservar el clear). - SDL_GPUColorTargetInfo color_target{}; - color_target.texture = offscreen_texture_; - color_target.load_op = SDL_GPU_LOADOP_LOAD; - color_target.store_op = SDL_GPU_STOREOP_STORE; - render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr); - - SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get()); - - // UBO de líneas usa el tamaño lógico (también del offscreen). - LineUniforms ubo{.viewport_width = logical_w_, - .viewport_height = logical_h_, - .padding_0 = 0.0F, - .padding_1 = 0.0F}; - SDL_PushGPUVertexUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo)); - - SDL_GPUBufferBinding vbo_binding{.buffer = vbo, .offset = 0}; - SDL_BindGPUVertexBuffers(render_pass_, 0, &vbo_binding, 1); - - SDL_GPUBufferBinding ibo_binding{.buffer = ibo, .offset = 0}; - SDL_BindGPUIndexBuffer(render_pass_, &ibo_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT); - - SDL_DrawGPUIndexedPrimitives(render_pass_, - static_cast(indices_.size()), - 1, 0, 0, 0); - - SDL_ReleaseGPUBuffer(dev, vbo); - SDL_ReleaseGPUBuffer(dev, ibo); - SDL_ReleaseGPUTransferBuffer(dev, tbo); -} - -void GpuFrameRenderer::compositePass() { - // Cierra el render pass actual (sobre offscreen). - if (render_pass_ != nullptr) { + // Copy pass FUERA del render pass. SDL_EndGPURenderPass(render_pass_); render_pass_ = nullptr; + + SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd_buffer_); + SDL_GPUTransferBufferLocation vbo_src{.transfer_buffer = tbo, .offset = 0}; + SDL_GPUBufferRegion vbo_dst{.buffer = vbo, .offset = 0, .size = VBO_SIZE}; + SDL_UploadToGPUBuffer(copy_pass, &vbo_src, &vbo_dst, false); + SDL_GPUTransferBufferLocation ibo_src{.transfer_buffer = tbo, .offset = VBO_SIZE}; + SDL_GPUBufferRegion ibo_dst{.buffer = ibo, .offset = 0, .size = IBO_SIZE}; + SDL_UploadToGPUBuffer(copy_pass, &ibo_src, &ibo_dst, false); + SDL_EndGPUCopyPass(copy_pass); + + // Reabrir render pass sobre OFFSCREEN (load_op=LOAD para preservar el clear). + SDL_GPUColorTargetInfo color_target{}; + color_target.texture = offscreen_texture_; + color_target.load_op = SDL_GPU_LOADOP_LOAD; + color_target.store_op = SDL_GPU_STOREOP_STORE; + render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr); + + SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get()); + + // UBO de líneas usa el tamaño lógico (también del offscreen). + LineUniforms ubo{.viewport_width = logical_w_, + .viewport_height = logical_h_, + .padding_0 = 0.0F, + .padding_1 = 0.0F}; + SDL_PushGPUVertexUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo)); + + SDL_GPUBufferBinding vbo_binding{.buffer = vbo, .offset = 0}; + SDL_BindGPUVertexBuffers(render_pass_, 0, &vbo_binding, 1); + + SDL_GPUBufferBinding ibo_binding{.buffer = ibo, .offset = 0}; + SDL_BindGPUIndexBuffer(render_pass_, &ibo_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT); + + SDL_DrawGPUIndexedPrimitives(render_pass_, + static_cast(indices_.size()), + 1, + 0, + 0, + 0); + + SDL_ReleaseGPUBuffer(dev, vbo); + SDL_ReleaseGPUBuffer(dev, ibo); + SDL_ReleaseGPUTransferBuffer(dev, tbo); } - // Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el - // letterbox del viewport físico). - SDL_GPUColorTargetInfo target{}; - target.texture = swapchain_texture_; - target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; - target.load_op = SDL_GPU_LOADOP_CLEAR; - target.store_op = SDL_GPU_STOREOP_STORE; - target.cycle = false; - render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr); - if (render_pass_ == nullptr) { - std::cerr << "[GpuFrameRenderer] BeginRenderPass (composite): " - << SDL_GetError() << '\n'; - return; + void GpuFrameRenderer::compositePass() { + // Cierra el render pass actual (sobre offscreen). + if (render_pass_ != nullptr) { + SDL_EndGPURenderPass(render_pass_); + render_pass_ = nullptr; + } + + // Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el + // letterbox del viewport físico). + SDL_GPUColorTargetInfo target{}; + target.texture = swapchain_texture_; + target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; + target.load_op = SDL_GPU_LOADOP_CLEAR; + target.store_op = SDL_GPU_STOREOP_STORE; + target.cycle = false; + render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr); + if (render_pass_ == nullptr) { + std::cerr << "[GpuFrameRenderer] BeginRenderPass (composite): " + << SDL_GetError() << '\n'; + return; + } + applyFinalViewport(); + + SDL_BindGPUGraphicsPipeline(render_pass_, postfx_pipeline_.get()); + + // Bind del sampler (escena offscreen) en slot 0 del fragment shader. + SDL_GPUTextureSamplerBinding sampler_binding{}; + sampler_binding.texture = offscreen_texture_; + sampler_binding.sampler = linear_sampler_; + SDL_BindGPUFragmentSamplers(render_pass_, 0, &sampler_binding, 1); + + // Uniforms del postpro. Si una sección está desactivada, anulamos sus + // contribuciones (intensidad / amplitud / max=min) en lugar de tener + // un branch en el shader. + const float BLOOM_INTENSITY = postfx_params_.bloom_enabled + ? postfx_params_.bloom_intensity + : 0.0F; + const float FLICKER_AMPLITUDE = postfx_params_.flicker_enabled + ? postfx_params_.flicker_amplitude + : 0.0F; + const float BG_MIN_R = postfx_params_.background_enabled ? postfx_params_.background_min_r : 0.0F; + const float BG_MIN_G = postfx_params_.background_enabled ? postfx_params_.background_min_g : 0.0F; + const float BG_MIN_B = postfx_params_.background_enabled ? postfx_params_.background_min_b : 0.0F; + const float BG_MAX_R = postfx_params_.background_enabled ? postfx_params_.background_max_r : 0.0F; + const float BG_MAX_G = postfx_params_.background_enabled ? postfx_params_.background_max_g : 0.0F; + const float BG_MAX_B = postfx_params_.background_enabled ? postfx_params_.background_max_b : 0.0F; + + // Tiempo en segundos desde el inicio de SDL (wall-clock real, robusto a FPS variables). + const float TIME_SECONDS = static_cast(SDL_GetTicks()) / 1000.0F; + + PostFxUniforms ubo{}; + ubo.time = TIME_SECONDS; + ubo.bloom_intensity = BLOOM_INTENSITY; + ubo.bloom_threshold = postfx_params_.bloom_threshold; + ubo.bloom_radius_px = postfx_params_.bloom_radius_px; + ubo.flicker_amplitude = FLICKER_AMPLITUDE; + ubo.flicker_frequency_hz = postfx_params_.flicker_frequency_hz; + ubo.background_pulse_freq_hz = postfx_params_.background_pulse_freq_hz; + ubo.pad_a = 0.0F; + ubo.background_min_r = BG_MIN_R; + ubo.background_min_g = BG_MIN_G; + ubo.background_min_b = BG_MIN_B; + ubo.background_min_a = 1.0F; + ubo.background_max_r = BG_MAX_R; + 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_; + ubo.pad_b = 0.0F; + ubo.pad_c = 0.0F; + + SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo)); + + // Fullscreen triangle: 3 vértices generados en el shader, sin VBO. + SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0); } - applyFinalViewport(); - SDL_BindGPUGraphicsPipeline(render_pass_, postfx_pipeline_.get()); - - // Bind del sampler (escena offscreen) en slot 0 del fragment shader. - SDL_GPUTextureSamplerBinding sampler_binding{}; - sampler_binding.texture = offscreen_texture_; - sampler_binding.sampler = linear_sampler_; - SDL_BindGPUFragmentSamplers(render_pass_, 0, &sampler_binding, 1); - - // Uniforms del postpro. Si una sección está desactivada, anulamos sus - // contribuciones (intensidad / amplitud / max=min) en lugar de tener - // un branch en el shader. - const float BLOOM_INTENSITY = postfx_params_.bloom_enabled - ? postfx_params_.bloom_intensity : 0.0F; - const float FLICKER_AMPLITUDE = postfx_params_.flicker_enabled - ? postfx_params_.flicker_amplitude : 0.0F; - const float BG_MIN_R = postfx_params_.background_enabled ? postfx_params_.background_min_r : 0.0F; - const float BG_MIN_G = postfx_params_.background_enabled ? postfx_params_.background_min_g : 0.0F; - const float BG_MIN_B = postfx_params_.background_enabled ? postfx_params_.background_min_b : 0.0F; - const float BG_MAX_R = postfx_params_.background_enabled ? postfx_params_.background_max_r : 0.0F; - const float BG_MAX_G = postfx_params_.background_enabled ? postfx_params_.background_max_g : 0.0F; - const float BG_MAX_B = postfx_params_.background_enabled ? postfx_params_.background_max_b : 0.0F; - - // Tiempo en segundos desde el inicio de SDL (wall-clock real, robusto a FPS variables). - const float TIME_SECONDS = static_cast(SDL_GetTicks()) / 1000.0F; - - PostFxUniforms ubo{}; - ubo.time = TIME_SECONDS; - ubo.bloom_intensity = BLOOM_INTENSITY; - ubo.bloom_threshold = postfx_params_.bloom_threshold; - ubo.bloom_radius_px = postfx_params_.bloom_radius_px; - ubo.flicker_amplitude = FLICKER_AMPLITUDE; - ubo.flicker_frequency_hz = postfx_params_.flicker_frequency_hz; - ubo.background_pulse_freq_hz = postfx_params_.background_pulse_freq_hz; - ubo.pad_a = 0.0F; - ubo.background_min_r = BG_MIN_R; - ubo.background_min_g = BG_MIN_G; - ubo.background_min_b = BG_MIN_B; - ubo.background_min_a = 1.0F; - ubo.background_max_r = BG_MAX_R; - 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_; - ubo.pad_b = 0.0F; - ubo.pad_c = 0.0F; - - SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo)); - - // Fullscreen triangle: 3 vértices generados en el shader, sin VBO. - SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0); -} - -void GpuFrameRenderer::endFrame() { - if (cmd_buffer_ == nullptr) { - return; + void GpuFrameRenderer::endFrame() { + if (cmd_buffer_ == nullptr) { + return; + } + flushBatch(); + compositePass(); + if (render_pass_ != nullptr) { + SDL_EndGPURenderPass(render_pass_); + render_pass_ = nullptr; + } + SDL_SubmitGPUCommandBuffer(cmd_buffer_); + cmd_buffer_ = nullptr; + swapchain_texture_ = nullptr; } - flushBatch(); - compositePass(); - if (render_pass_ != nullptr) { - SDL_EndGPURenderPass(render_pass_); - render_pass_ = nullptr; - } - SDL_SubmitGPUCommandBuffer(cmd_buffer_); - cmd_buffer_ = nullptr; - swapchain_texture_ = nullptr; -} } // namespace Rendering::GPU diff --git a/source/core/rendering/gpu/gpu_frame_renderer.hpp b/source/core/rendering/gpu/gpu_frame_renderer.hpp index 376c1c5..2b2174d 100644 --- a/source/core/rendering/gpu/gpu_frame_renderer.hpp +++ b/source/core/rendering/gpu/gpu_frame_renderer.hpp @@ -27,30 +27,30 @@ namespace Rendering::GPU { -// Parámetros del postpro que el caller (SDLManager) pasa cada frame. -// Equivalente al lado "humano" del PostFxUniforms (sin paddings). -struct PostFxParams { - bool bloom_enabled{true}; - float bloom_intensity{0.6F}; - float bloom_threshold{0.4F}; - float bloom_radius_px{2.0F}; + // Parámetros del postpro que el caller (SDLManager) pasa cada frame. + // Equivalente al lado "humano" del PostFxUniforms (sin paddings). + struct PostFxParams { + bool bloom_enabled{true}; + float bloom_intensity{0.6F}; + float bloom_threshold{0.4F}; + float bloom_radius_px{2.0F}; - bool flicker_enabled{true}; - float flicker_amplitude{0.10F}; - float flicker_frequency_hz{6.0F}; + bool flicker_enabled{true}; + float flicker_amplitude{0.10F}; + float flicker_frequency_hz{6.0F}; - bool background_enabled{true}; - float background_min_r{0.0F}; - float background_min_g{0.02F}; - float background_min_b{0.0F}; - float background_max_r{0.0F}; - float background_max_g{0.06F}; - float background_max_b{0.0F}; - float background_pulse_freq_hz{6.0F}; -}; + bool background_enabled{true}; + float background_min_r{0.0F}; + float background_min_g{0.02F}; + float background_min_b{0.0F}; + float background_max_r{0.0F}; + float background_max_g{0.06F}; + float background_max_b{0.0F}; + float background_pulse_freq_hz{6.0F}; + }; -class GpuFrameRenderer { - public: + class GpuFrameRenderer { + public: GpuFrameRenderer() = default; ~GpuFrameRenderer(); @@ -72,8 +72,7 @@ class GpuFrameRenderer { [[nodiscard]] auto beginFrame(float clear_r, float clear_g, float clear_b) -> bool; // Encola una línea con grosor configurable (px). Color RGBA en [0..1]. - void pushLine(float x1, float y1, float x2, float y2, float thickness, - float r, float g, float b, float a); + void pushLine(float x1, float y1, float x2, float y2, float thickness, float r, float g, float b, float a); // endFrame: flush del batch de líneas → composite postpro → submit + presenta. void endFrame(); @@ -87,6 +86,13 @@ class GpuFrameRenderer { // Activa/desactiva VSync. true = SDL_GPU_PRESENTMODE_VSYNC, false = IMMEDIATE. void setVSync(bool enabled); + // Activa/desactiva l'antialias geomètric a les línies. Quan està OFF, + // pushLine no extrudeix píxel extra ni emet edge_dist (geometria com + // abans del commit d'AA-1). Quan està ON, l'extrusió i el fade del + // fragment shader produeixen bords suavitzats. + void setAntialias(bool enabled) { antialias_enabled_ = enabled; } + [[nodiscard]] auto isAntialiasEnabled() const -> bool { return antialias_enabled_; } + // Parámetros del postpro que se aplican en endFrame. Por defecto = // valores de Defaults (bloom moderado, flicker suave, fondo verde tenue). void setPostFx(const PostFxParams& params) { postfx_params_ = params; } @@ -96,7 +102,7 @@ class GpuFrameRenderer { [[nodiscard]] auto device() -> GpuDevice& { return device_; } [[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; } - private: + private: GpuDevice device_; GpuLinePipeline line_pipeline_; GpuPostFxPipeline postfx_pipeline_; @@ -128,12 +134,15 @@ class GpuFrameRenderer { // Parámetros del postpro (configurables vía YAML). PostFxParams postfx_params_{}; + // Estat de l'antialias geomètric a les línies (toggle F5). + bool antialias_enabled_{true}; + // Helpers internos. [[nodiscard]] auto createOffscreen() -> bool; void destroyOffscreen(); void flushBatch(); void compositePass(); void applyFinalViewport(); -}; + }; } // namespace Rendering::GPU diff --git a/source/core/rendering/gpu/gpu_line_pipeline.cpp b/source/core/rendering/gpu/gpu_line_pipeline.cpp index 38705bf..58bab9d 100644 --- a/source/core/rendering/gpu/gpu_line_pipeline.cpp +++ b/source/core/rendering/gpu/gpu_line_pipeline.cpp @@ -11,107 +11,111 @@ namespace Rendering::GPU { -GpuLinePipeline::~GpuLinePipeline() { destroy(); } + GpuLinePipeline::~GpuLinePipeline() { destroy(); } -auto GpuLinePipeline::init(const GpuDevice& device, - SDL_GPUTextureFormat target_format) -> bool { - owner_ = device.get(); - if (owner_ == nullptr) { - return false; - } - - SDL_GPUShader* vert = device.loadShader("line.vert.spv", - SDL_GPU_SHADERSTAGE_VERTEX, - /*num_uniform_buffers=*/1); - SDL_GPUShader* frag = device.loadShader("line.frag.spv", - SDL_GPU_SHADERSTAGE_FRAGMENT, - /*num_uniform_buffers=*/0); - if ((vert == nullptr) || (frag == nullptr)) { - if (vert != nullptr) { - SDL_ReleaseGPUShader(owner_, vert); + auto GpuLinePipeline::init(const GpuDevice& device, + SDL_GPUTextureFormat target_format) -> bool { + owner_ = device.get(); + if (owner_ == nullptr) { + return false; } - if (frag != nullptr) { - SDL_ReleaseGPUShader(owner_, frag); + + SDL_GPUShader* vert = device.loadShader("line.vert.spv", + SDL_GPU_SHADERSTAGE_VERTEX, + /*num_uniform_buffers=*/1); + SDL_GPUShader* frag = device.loadShader("line.frag.spv", + SDL_GPU_SHADERSTAGE_FRAGMENT, + /*num_uniform_buffers=*/0); + if ((vert == nullptr) || (frag == nullptr)) { + if (vert != nullptr) { + SDL_ReleaseGPUShader(owner_, vert); + } + if (frag != nullptr) { + SDL_ReleaseGPUShader(owner_, frag); + } + std::cerr << "[GpuLinePipeline] Error cargando shaders\n"; + return false; } - std::cerr << "[GpuLinePipeline] Error cargando shaders\n"; - return false; + + // Vertex layout: pos (vec2) + color (vec4) + edge_dist (float) → 7 floats per vèrtex. + SDL_GPUVertexBufferDescription vbo_desc{}; + vbo_desc.slot = 0; + vbo_desc.pitch = sizeof(LineVertex); + vbo_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; + vbo_desc.instance_step_rate = 0; + + SDL_GPUVertexAttribute attrs[3]; + attrs[0].location = 0; // in_position + attrs[0].buffer_slot = 0; + attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; + attrs[0].offset = 0; + attrs[1].location = 1; // in_color + attrs[1].buffer_slot = 0; + attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; + attrs[1].offset = sizeof(float) * 2; + attrs[2].location = 2; // in_edge_dist + attrs[2].buffer_slot = 0; + attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT; + attrs[2].offset = sizeof(float) * 6; + + SDL_GPUVertexInputState vertex_input{}; + vertex_input.vertex_buffer_descriptions = &vbo_desc; + vertex_input.num_vertex_buffers = 1; + vertex_input.vertex_attributes = attrs; + vertex_input.num_vertex_attributes = 3; + + // Color target = formato pasado por el caller (offscreen u otro). + // Blending alpha estándar. + SDL_GPUColorTargetDescription color_target{}; + color_target.format = target_format; + color_target.blend_state.enable_blend = true; + color_target.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; + color_target.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + color_target.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; + color_target.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; + color_target.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + color_target.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; + color_target.blend_state.color_write_mask = + SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G | + SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A; + + SDL_GPUGraphicsPipelineTargetInfo target_info{}; + target_info.color_target_descriptions = &color_target; + target_info.num_color_targets = 1; + target_info.has_depth_stencil_target = false; + + SDL_GPUGraphicsPipelineCreateInfo info{}; + info.vertex_shader = vert; + info.fragment_shader = frag; + info.vertex_input_state = vertex_input; + info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; + info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; // No backface culling para 2D + info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; + info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1; + info.depth_stencil_state = {}; + info.target_info = target_info; + + pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info); + + // Los shaders se pueden liberar tras crear el pipeline (SDL los retiene + // internamente mientras el pipeline esté vivo). + SDL_ReleaseGPUShader(owner_, vert); + SDL_ReleaseGPUShader(owner_, frag); + + if (pipeline_ == nullptr) { + std::cerr << "[GpuLinePipeline] SDL_CreateGPUGraphicsPipeline: " << SDL_GetError() << '\n'; + return false; + } + return true; } - // Vertex layout: pos (vec2) + color (vec4) → 6 floats por vertex. - SDL_GPUVertexBufferDescription vbo_desc{}; - vbo_desc.slot = 0; - vbo_desc.pitch = sizeof(LineVertex); - vbo_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; - vbo_desc.instance_step_rate = 0; - - SDL_GPUVertexAttribute attrs[2]; - attrs[0].location = 0; // in_position - attrs[0].buffer_slot = 0; - attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; - attrs[0].offset = 0; - attrs[1].location = 1; // in_color - attrs[1].buffer_slot = 0; - attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; - attrs[1].offset = sizeof(float) * 2; - - SDL_GPUVertexInputState vertex_input{}; - vertex_input.vertex_buffer_descriptions = &vbo_desc; - vertex_input.num_vertex_buffers = 1; - vertex_input.vertex_attributes = attrs; - vertex_input.num_vertex_attributes = 2; - - // Color target = formato pasado por el caller (offscreen u otro). - // Blending alpha estándar. - SDL_GPUColorTargetDescription color_target{}; - color_target.format = target_format; - color_target.blend_state.enable_blend = true; - color_target.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; - color_target.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - color_target.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; - color_target.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; - color_target.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - color_target.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; - color_target.blend_state.color_write_mask = - SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G | - SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A; - - SDL_GPUGraphicsPipelineTargetInfo target_info{}; - target_info.color_target_descriptions = &color_target; - target_info.num_color_targets = 1; - target_info.has_depth_stencil_target = false; - - SDL_GPUGraphicsPipelineCreateInfo info{}; - info.vertex_shader = vert; - info.fragment_shader = frag; - info.vertex_input_state = vertex_input; - info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; - info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; // No backface culling para 2D - info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; - info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1; - info.depth_stencil_state = {}; - info.target_info = target_info; - - pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info); - - // Los shaders se pueden liberar tras crear el pipeline (SDL los retiene - // internamente mientras el pipeline esté vivo). - SDL_ReleaseGPUShader(owner_, vert); - SDL_ReleaseGPUShader(owner_, frag); - - if (pipeline_ == nullptr) { - std::cerr << "[GpuLinePipeline] SDL_CreateGPUGraphicsPipeline: " << SDL_GetError() << '\n'; - return false; + void GpuLinePipeline::destroy() { + if ((pipeline_ != nullptr) && (owner_ != nullptr)) { + SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_); + } + pipeline_ = nullptr; + owner_ = nullptr; } - return true; -} - -void GpuLinePipeline::destroy() { - if ((pipeline_ != nullptr) && (owner_ != nullptr)) { - SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_); - } - pipeline_ = nullptr; - owner_ = nullptr; -} } // namespace Rendering::GPU diff --git a/source/core/rendering/gpu/gpu_line_pipeline.hpp b/source/core/rendering/gpu/gpu_line_pipeline.hpp index da312e6..be7177d 100644 --- a/source/core/rendering/gpu/gpu_line_pipeline.hpp +++ b/source/core/rendering/gpu/gpu_line_pipeline.hpp @@ -11,28 +11,34 @@ namespace Rendering::GPU { -class GpuDevice; + class GpuDevice; -// Vertex layout (debe coincidir con shaders/line.vert.glsl). -struct LineVertex { + // Vertex layout (debe coincidir con shaders/line.vert.glsl). + // + // edge_dist: distància normalitzada a l'eix central de la línia, ±1 als bords + // i 0 al centre. El fragment shader la fa servir per fer un fade als bords + // (antialias geomètric). Els vèrtexs del costat "+normal" porten +1 i els del + // costat "-normal" porten -1. + struct LineVertex { float x; float y; float r; float g; float b; float a; -}; + float edge_dist; + }; -// Uniform buffer del vertex shader (debe coincidir con UBO en line.vert.glsl). -struct LineUniforms { + // Uniform buffer del vertex shader (debe coincidir con UBO en line.vert.glsl). + struct LineUniforms { float viewport_width; float viewport_height; float padding_0; float padding_1; // Alineamiento a 16 bytes -}; + }; -class GpuLinePipeline { - public: + class GpuLinePipeline { + public: GpuLinePipeline() = default; ~GpuLinePipeline(); @@ -45,14 +51,14 @@ class GpuLinePipeline { // (swapchain o offscreen). Por defecto coincide con el del swapchain // del device. [[nodiscard]] auto init(const GpuDevice& device, - SDL_GPUTextureFormat target_format) -> bool; + SDL_GPUTextureFormat target_format) -> bool; void destroy(); [[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; } - private: + private: SDL_GPUDevice* owner_{nullptr}; // No-owning; el GpuDevice es el dueño SDL_GPUGraphicsPipeline* pipeline_{nullptr}; -}; + }; } // namespace Rendering::GPU diff --git a/source/core/rendering/sdl_manager.cpp b/source/core/rendering/sdl_manager.cpp index 47c5416..3bf8060 100644 --- a/source/core/rendering/sdl_manager.cpp +++ b/source/core/rendering/sdl_manager.cpp @@ -84,6 +84,9 @@ SDLManager::SDLManager(int width, int height, bool fullscreen, Config::EngineCon 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. @@ -173,10 +176,17 @@ void SDLManager::applyZoom(float new_zoom) { } void SDLManager::updateViewport() { - // Cálculo de letterbox: el juego se renderiza a 1280×720 lógicos, pero - // la swapchain tiene el tamaño físico de la ventana. Aplicamos un viewport - // centrado con la proporción 16:9 para preservar aspect ratio. - float scale = zoom_factor_; + // 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)); @@ -247,6 +257,10 @@ void SDLManager::toggleFullscreen() { 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); std::cout << "F3: Fullscreen activat (guardada: " << windowed_width_ << "x" << windowed_height_ << ")" << '\n'; @@ -266,11 +280,13 @@ auto SDLManager::handleWindowEvent(const SDL_Event& event) -> bool { if (event.type == SDL_EVENT_WINDOW_RESIZED) { SDL_GetWindowSize(finestra_, ¤t_width_, ¤t_height_); - float new_zoom = static_cast(current_width_) / Defaults::Window::WIDTH; - zoom_factor_ = std::max(Defaults::Window::MIN_ZOOM, - std::min(new_zoom, max_zoom_)); - + // 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_; } @@ -310,3 +326,11 @@ void SDLManager::toggleVSync() { on_persist_(); } } + +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). + std::cout << "F5: AA " << (cfg_->rendering.antialias != 0 ? "ON" : "OFF") << '\n'; +} diff --git a/source/core/rendering/sdl_manager.hpp b/source/core/rendering/sdl_manager.hpp index f5b0dfe..ca71bee 100644 --- a/source/core/rendering/sdl_manager.hpp +++ b/source/core/rendering/sdl_manager.hpp @@ -34,6 +34,7 @@ class SDLManager { void decreaseWindowSize(); // F1: -100px void toggleFullscreen(); // F3 void toggleVSync(); // F4 + void toggleAntialias(); // F5 auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED // Funciones principals (renderizado). diff --git a/source/core/system/debug_overlay.cpp b/source/core/system/debug_overlay.cpp index 5e81d09..e8f2577 100644 --- a/source/core/system/debug_overlay.cpp +++ b/source/core/system/debug_overlay.cpp @@ -44,6 +44,7 @@ namespace System { const std::string FPS_TEXT = "FPS: " + std::to_string(fps_display_); const std::string VSYNC_TEXT = std::string("VSYNC: ") + (rendering_cfg_->vsync == 1 ? "ON" : "OFF"); + const std::string AA_TEXT = std::string("AA: ") + (rendering_cfg_->antialias == 1 ? "ON" : "OFF"); text_.render(FPS_TEXT, Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS}, @@ -55,6 +56,11 @@ namespace System { OVERLAY_SCALE, OVERLAY_SPACING, OVERLAY_BRIGHTNESS); + text_.render(AA_TEXT, + Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS + (2.0F * OVERLAY_LINE_HEIGHT)}, + OVERLAY_SCALE, + OVERLAY_SPACING, + OVERLAY_BRIGHTNESS); } } // namespace System diff --git a/source/core/system/global_events.cpp b/source/core/system/global_events.cpp index 1eb4fcc..3615126 100644 --- a/source/core/system/global_events.cpp +++ b/source/core/system/global_events.cpp @@ -5,10 +5,10 @@ #include -#include "scene_context.hpp" #include "core/input/input.hpp" #include "core/input/mouse.hpp" #include "core/rendering/sdl_manager.hpp" +#include "scene_context.hpp" // Using declarations per simplificar el codi using SceneManager::SceneContext; @@ -16,55 +16,59 @@ using SceneType = SceneContext::SceneType; namespace GlobalEvents { -auto handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) -> bool { - // 1. Permitir que Input procese el evento (para hotplug de gamepads) - auto event_msg = Input::get()->handleEvent(event); - if (!event_msg.empty()) { - std::cout << "[Input] " << event_msg << '\n'; - } - - // 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego) - if (event.type == SDL_EVENT_QUIT) { - context.setNextScene(SceneType::EXIT); - SceneManager::actual = SceneType::EXIT; - return true; - } - - // 3. Gestió del ratolí (auto-ocultar) - Mouse::handleEvent(event); - - // 4. Procesar acciones globales directamente desde eventos SDL - // (NO usar Input::checkAction() para evitar desfase de timing) - if (event.type == SDL_EVENT_KEY_DOWN) { - switch (event.key.scancode) { - case SDL_SCANCODE_F1: - sdl.decreaseWindowSize(); - return true; - - case SDL_SCANCODE_F2: - sdl.increaseWindowSize(); - return true; - - case SDL_SCANCODE_F3: - sdl.toggleFullscreen(); - return true; - - case SDL_SCANCODE_F4: - sdl.toggleVSync(); - return true; - - case SDL_SCANCODE_ESCAPE: - context.setNextScene(SceneType::EXIT); - SceneManager::actual = SceneType::EXIT; - return true; - - default: - // Tecla no global - break; + auto handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) -> bool { + // 1. Permitir que Input procese el evento (para hotplug de gamepads) + auto event_msg = Input::get()->handleEvent(event); + if (!event_msg.empty()) { + std::cout << "[Input] " << event_msg << '\n'; } - } - return false; // Event no processat -} + // 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego) + if (event.type == SDL_EVENT_QUIT) { + context.setNextScene(SceneType::EXIT); + SceneManager::actual = SceneType::EXIT; + return true; + } + + // 3. Gestió del ratolí (auto-ocultar) + Mouse::handleEvent(event); + + // 4. Procesar acciones globales directamente desde eventos SDL + // (NO usar Input::checkAction() para evitar desfase de timing) + if (event.type == SDL_EVENT_KEY_DOWN) { + switch (event.key.scancode) { + case SDL_SCANCODE_F1: + sdl.decreaseWindowSize(); + return true; + + case SDL_SCANCODE_F2: + sdl.increaseWindowSize(); + return true; + + case SDL_SCANCODE_F3: + sdl.toggleFullscreen(); + return true; + + case SDL_SCANCODE_F4: + sdl.toggleVSync(); + return true; + + case SDL_SCANCODE_F5: + sdl.toggleAntialias(); + return true; + + case SDL_SCANCODE_ESCAPE: + context.setNextScene(SceneType::EXIT); + SceneManager::actual = SceneType::EXIT; + return true; + + default: + // Tecla no global + break; + } + } + + return false; // Event no processat + } } // namespace GlobalEvents