fix(vsync): comprovar suport de present mode i loggejar el mode efectiu

setVSync demanava SDL_GPU_PRESENTMODE_IMMEDIATE sense comprovar suport.
A SDL_GPU només VSYNC està garantit; IMMEDIATE i MAILBOX són opcionals.
Si no estaven suportats (típicament Wayland/X11 amb compositor), SDL
retornava error i la swapchain es quedava en VSYNC sense que ho sabéssim.

Ara:
- Consultem SDL_WindowSupportsGPUPresentMode abans de fer la crida.
- En VSync OFF: provem IMMEDIATE → fallback a MAILBOX → si cap, ens
  quedem en VSYNC i avisem (driver/compositor força VSync).
- Loggejem sempre el mode efectiu (no només els errors), perquè ara mateix
  no hi havia forma de saber des de fora si el toggle havia tingut efecte.
This commit is contained in:
2026-05-20 20:17:28 +02:00
parent 7c2499cd91
commit 6063309932
+385 -350
View File
@@ -12,389 +12,424 @@
namespace Rendering::GPU { namespace Rendering::GPU {
GpuFrameRenderer::~GpuFrameRenderer() { destroy(); } GpuFrameRenderer::~GpuFrameRenderer() { destroy(); }
auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h) -> bool { auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h) -> bool {
logical_w_ = logical_w; logical_w_ = logical_w;
logical_h_ = logical_h; logical_h_ = logical_h;
if (!device_.init(window)) { if (!device_.init(window)) {
return false; 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_)) { auto GpuFrameRenderer::createOffscreen() -> bool {
device_.destroy(); SDL_GPUDevice* dev = device_.get();
return false; 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())) { void GpuFrameRenderer::destroyOffscreen() {
line_pipeline_.destroy(); SDL_GPUDevice* dev = device_.get();
device_.destroy(); if (dev == nullptr) {
return false; 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(); postfx_pipeline_.destroy();
line_pipeline_.destroy(); line_pipeline_.destroy();
device_.destroy(); device_.destroy();
return false; vertices_.clear();
} indices_.clear();
return true;
}
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. auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) -> bool {
SDL_GPUTextureCreateInfo tex_info{}; // Los clear_* se ignoran: el fondo lo pinta el postpro. Mantenemos la
tex_info.type = SDL_GPU_TEXTURETYPE_2D; // firma para no romper el SDLManager.
tex_info.format = offscreen_format_; (void)clear_r;
tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; (void)clear_g;
tex_info.width = static_cast<uint32_t>(logical_w_); (void)clear_b;
tex_info.height = static_cast<uint32_t>(logical_h_);
tex_info.layer_count_or_depth = 1; SDL_GPUDevice* dev = device_.get();
tex_info.num_levels = 1; if (dev == nullptr) {
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1; return false;
offscreen_texture_ = SDL_CreateGPUTexture(dev, &tex_info); }
if (offscreen_texture_ == nullptr) {
std::cerr << "[GpuFrameRenderer] SDL_CreateGPUTexture (offscreen): " cmd_buffer_ = SDL_AcquireGPUCommandBuffer(dev);
<< SDL_GetError() << '\n'; if (cmd_buffer_ == nullptr) {
return false; 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). void GpuFrameRenderer::setViewport(float x, float y, float w, float h) {
SDL_GPUSamplerCreateInfo sampler_info{}; viewport_x_ = x;
sampler_info.min_filter = SDL_GPU_FILTER_LINEAR; viewport_y_ = y;
sampler_info.mag_filter = SDL_GPU_FILTER_LINEAR; viewport_w_ = w;
sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR; viewport_h_ = h;
sampler_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; // El viewport solo se aplica en el pase final (composite). Si estamos
sampler_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; // ya dentro del composite, lo aplicaríamos inmediatamente, pero la API
sampler_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; // está pensada para llamarse antes de endFrame/al cambiar de ventana.
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;
} }
cmd_buffer_ = SDL_AcquireGPUCommandBuffer(dev); void GpuFrameRenderer::setVSync(bool enabled) {
if (cmd_buffer_ == nullptr) { SDL_GPUDevice* dev = device_.get();
std::cerr << "[GpuFrameRenderer] SDL_AcquireGPUCommandBuffer: " << SDL_GetError() << '\n'; SDL_Window* win = device_.window();
return false; 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(), void GpuFrameRenderer::applyFinalViewport() {
&swapchain_texture_, nullptr, nullptr)) { if (render_pass_ == nullptr) {
std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n'; return;
SDL_SubmitGPUCommandBuffer(cmd_buffer_); }
cmd_buffer_ = nullptr; if (viewport_w_ <= 0.0F || viewport_h_ <= 0.0F) {
return false; 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) { void GpuFrameRenderer::pushLine(float x1, float y1, float x2, float y2, float thickness, float r, float g, float b, float a) {
// Ventana minimizada o swapchain no disponible: solo submit y salir. const float DX = x2 - x1;
SDL_SubmitGPUCommandBuffer(cmd_buffer_); const float DY = y2 - y1;
cmd_buffer_ = nullptr; const float LEN = std::sqrt((DX * DX) + (DY * DY));
return false; if (LEN < 1e-6F) {
return;
}
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);
} }
// Abrir render pass sobre OFFSCREEN con clear a negro. void GpuFrameRenderer::flushBatch() {
SDL_GPUColorTargetInfo color_target{}; if (vertices_.empty() || indices_.empty()) {
color_target.texture = offscreen_texture_; return;
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); SDL_GPUDevice* dev = device_.get();
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(); const auto VBO_SIZE = static_cast<uint32_t>(vertices_.size() * sizeof(LineVertex));
indices_.clear(); const auto IBO_SIZE = static_cast<uint32_t>(indices_.size() * sizeof(uint16_t));
return true;
}
void GpuFrameRenderer::setViewport(float x, float y, float w, float h) { SDL_GPUBufferCreateInfo vbo_info{};
viewport_x_ = x; vbo_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
viewport_y_ = y; vbo_info.size = VBO_SIZE;
viewport_w_ = w; SDL_GPUBuffer* vbo = SDL_CreateGPUBuffer(dev, &vbo_info);
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.
}
void GpuFrameRenderer::setVSync(bool enabled) { SDL_GPUBufferCreateInfo ibo_info{};
SDL_GPUDevice* dev = device_.get(); ibo_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
if (dev == nullptr || device_.window() == nullptr) { ibo_info.size = IBO_SIZE;
return; SDL_GPUBuffer* ibo = SDL_CreateGPUBuffer(dev, &ibo_info);
}
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';
}
}
void GpuFrameRenderer::applyFinalViewport() { SDL_GPUTransferBufferCreateInfo tbo_info{};
if (render_pass_ == nullptr) { tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
return; tbo_info.size = VBO_SIZE + IBO_SIZE;
} SDL_GPUTransferBuffer* tbo = SDL_CreateGPUTransferBuffer(dev, &tbo_info);
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);
}
void GpuFrameRenderer::pushLine(float x1, float y1, float x2, float y2, float thickness, auto* mapped = static_cast<uint8_t*>(SDL_MapGPUTransferBuffer(dev, tbo, false));
float r, float g, float b, float a) { std::memcpy(mapped, vertices_.data(), VBO_SIZE);
const float DX = x2 - x1; std::memcpy(mapped + VBO_SIZE, indices_.data(), IBO_SIZE);
const float DY = y2 - y1; SDL_UnmapGPUTransferBuffer(dev, tbo);
const float LEN = std::sqrt((DX * DX) + (DY * DY));
if (LEN < 1e-6F) {
return;
}
const float HALF = thickness * 0.5F; // Copy pass FUERA del render pass.
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) {
SDL_EndGPURenderPass(render_pass_); SDL_EndGPURenderPass(render_pass_);
render_pass_ = nullptr; 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 void GpuFrameRenderer::compositePass() {
// letterbox del viewport físico). // Cierra el render pass actual (sobre offscreen).
SDL_GPUColorTargetInfo target{}; if (render_pass_ != nullptr) {
target.texture = swapchain_texture_; SDL_EndGPURenderPass(render_pass_);
target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; render_pass_ = nullptr;
target.load_op = SDL_GPU_LOADOP_CLEAR; }
target.store_op = SDL_GPU_STOREOP_STORE;
target.cycle = false; // Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el
render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr); // letterbox del viewport físico).
if (render_pass_ == nullptr) { SDL_GPUColorTargetInfo target{};
std::cerr << "[GpuFrameRenderer] BeginRenderPass (composite): " target.texture = swapchain_texture_;
<< SDL_GetError() << '\n'; target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
return; 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()); void GpuFrameRenderer::endFrame() {
if (cmd_buffer_ == nullptr) {
// Bind del sampler (escena offscreen) en slot 0 del fragment shader. return;
SDL_GPUTextureSamplerBinding sampler_binding{}; }
sampler_binding.texture = offscreen_texture_; flushBatch();
sampler_binding.sampler = linear_sampler_; compositePass();
SDL_BindGPUFragmentSamplers(render_pass_, 0, &sampler_binding, 1); if (render_pass_ != nullptr) {
SDL_EndGPURenderPass(render_pass_);
// Uniforms del postpro. Si una sección está desactivada, anulamos sus render_pass_ = nullptr;
// contribuciones (intensidad / amplitud / max=min) en lugar de tener }
// un branch en el shader. SDL_SubmitGPUCommandBuffer(cmd_buffer_);
const float BLOOM_INTENSITY = postfx_params_.bloom_enabled cmd_buffer_ = nullptr;
? postfx_params_.bloom_intensity : 0.0F; swapchain_texture_ = nullptr;
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;
} }
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 } // namespace Rendering::GPU