Merge branch 'fix/vsync-fullscreen-antialias': viewport fullscreen, VSync fallback i AA geomètric

Tres fronts arreglats a la rama:

- Fullscreen: el viewport ja no depèn de zoom_factor_ (capat per max_zoom_),
  sinó que és aspect-fit de la mida física actual. Afegit
  SetWindowFullscreenMode(nullptr) i preservació del zoom_factor_ de windowed
  durant transicions.
- VSync: setVSync ara consulta SDL_WindowSupportsGPUPresentMode i fa fallback
  IMMEDIATE → MAILBOX → VSYNC quan el driver/compositor força VSync. Loggeja
  el mode efectiu.
- Antialias geomètric a les línies: edge attribute + smoothstep al fragment.
  Toggle runtime amb F5, indicador 'AA: ON/OFF' al debug overlay (F11).
This commit is contained in:
2026-05-20 21:42:53 +02:00
14 changed files with 664 additions and 548 deletions
+17 -4
View File
@@ -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);
}
+5 -2
View File
@@ -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;
}
+2 -1
View File
@@ -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 {
+2 -1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+392 -350
View File
@@ -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<uint32_t>(logical_w_);
tex_info.height = static_cast<uint32_t>(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<uint32_t>(logical_w_);
tex_info.height = static_cast<uint32_t>(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<uint16_t>(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<uint32_t>(vertices_.size() * sizeof(LineVertex));
const auto IBO_SIZE = static_cast<uint32_t>(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<uint8_t*>(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<uint16_t>(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<uint32_t>(vertices_.size() * sizeof(LineVertex));
const auto IBO_SIZE = static_cast<uint32_t>(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<uint8_t*>(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<uint32_t>(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<uint32_t>(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<float>(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<float>(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
@@ -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
+100 -96
View File
@@ -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
+18 -12
View File
@@ -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
+32 -8
View File
@@ -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<float>(current_width_) / Defaults::Game::WIDTH;
float scale_h = static_cast<float>(current_height_) / Defaults::Game::HEIGHT;
float scale = std::min(scale_w, scale_h);
int scaled_width = static_cast<int>(std::round(Defaults::Game::WIDTH * scale));
int scaled_height = static_cast<int>(std::round(Defaults::Game::HEIGHT * scale));
@@ -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_, &current_width_, &current_height_);
float new_zoom = static_cast<float>(current_width_) / Defaults::Window::WIDTH;
zoom_factor_ = std::max(Defaults::Window::MIN_ZOOM,
std::min(new_zoom, max_zoom_));
// En fullscreen el zoom_factor_ no participa del viewport (aspect-fit
// sobre la mida física), així que el preservem amb el valor de
// windowed per no perdre'l en tornar a windowed.
if (!is_fullscreen_) {
float new_zoom = static_cast<float>(current_width_) / Defaults::Window::WIDTH;
zoom_factor_ = std::max(Defaults::Window::MIN_ZOOM,
std::min(new_zoom, max_zoom_));
windowed_width_ = current_width_;
windowed_height_ = current_height_;
}
@@ -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';
}
+1
View File
@@ -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).
+6
View File
@@ -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
+53 -49
View File
@@ -5,10 +5,10 @@
#include <iostream>
#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