Fase 8c: postpro (bloom + flicker + background) en SDL_gpu
Renderiza la escena de líneas a una textura offscreen y aplica un pase
final de postpro que compone la imagen al swapchain. El shader del
postpro hace tres cosas:
- Bloom: kernel gaussiano 5×5 con high-pass por luminancia. Configurable
vía intensity, threshold y radius_px.
- Flicker: multiplicador global de brillo modulado por sin(time*freq).
Sustituye al antiguo ColorOscillator CPU; eliminados oscillator.{hpp,cpp}
y Defaults::Color. SDLManager::updateColors queda como no-op para no
tocar las escenas que lo invocaban.
- Background pulse: color de fondo aditivo entre color_min y color_max,
pulsando en el tiempo.
Parámetros expuestos en data/config/postfx.yaml y cargados con fkYAML.
Si el archivo falta o falla, se usan defaults built-in. UV.y invertida
en el vertex shader del postpro para compensar la convención de
muestreo de SDL_gpu/Vulkan (el line shader sigue con su ndc.y flip).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,8 @@ void GpuDevice::destroy() {
|
||||
|
||||
auto GpuDevice::loadShader(const std::string& spv_filename,
|
||||
SDL_GPUShaderStage stage,
|
||||
uint32_t num_uniform_buffers) const -> SDL_GPUShader* {
|
||||
uint32_t num_uniform_buffers,
|
||||
uint32_t num_samplers) const -> SDL_GPUShader* {
|
||||
if (device_ == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -90,7 +91,7 @@ auto GpuDevice::loadShader(const std::string& spv_filename,
|
||||
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
info.stage = stage;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
info.num_samplers = 0;
|
||||
info.num_samplers = num_samplers;
|
||||
info.num_storage_buffers = 0;
|
||||
info.num_storage_textures = 0;
|
||||
|
||||
|
||||
@@ -40,9 +40,11 @@ class GpuDevice {
|
||||
// SDL_ReleaseGPUShader). Retorna nullptr si falla.
|
||||
//
|
||||
// num_uniform_buffers: nº de uniform buffers que usa el shader (slot 0..N-1).
|
||||
// num_samplers: nº de samplers (combined image+sampler) usados por el shader.
|
||||
[[nodiscard]] auto loadShader(const std::string& spv_filename,
|
||||
SDL_GPUShaderStage stage,
|
||||
uint32_t num_uniform_buffers) const -> SDL_GPUShader*;
|
||||
uint32_t num_uniform_buffers,
|
||||
uint32_t num_samplers = 0) const -> SDL_GPUShader*;
|
||||
|
||||
[[nodiscard]] auto get() const -> SDL_GPUDevice* { return device_; }
|
||||
[[nodiscard]] auto window() const -> SDL_Window* { return window_; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// gpu_frame_renderer.cpp - Implementación del FrameRenderer
|
||||
// gpu_frame_renderer.cpp - Implementación del FrameRenderer con offscreen + postpro
|
||||
|
||||
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
|
||||
|
||||
@@ -21,21 +21,99 @@ auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h
|
||||
if (!device_.init(window)) {
|
||||
return false;
|
||||
}
|
||||
if (!pipeline_.init(device_)) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
pipeline_.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;
|
||||
@@ -62,21 +140,23 @@ auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) -
|
||||
return false;
|
||||
}
|
||||
|
||||
// Abrir render pass sobre OFFSCREEN con clear a negro.
|
||||
SDL_GPUColorTargetInfo color_target{};
|
||||
color_target.texture = swapchain_texture_;
|
||||
color_target.clear_color = SDL_FColor{.r = clear_r, .g = clear_g, .b = clear_b, .a = 1.0F};
|
||||
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: " << SDL_GetError() << '\n';
|
||||
std::cerr << "[GpuFrameRenderer] SDL_BeginGPURenderPass (offscreen): "
|
||||
<< SDL_GetError() << '\n';
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
||||
cmd_buffer_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
applyViewport();
|
||||
// Sin SetGPUViewport: el offscreen se llena entero a tamaño lógico.
|
||||
|
||||
vertices_.clear();
|
||||
indices_.clear();
|
||||
@@ -88,10 +168,9 @@ void GpuFrameRenderer::setViewport(float x, float y, float w, float h) {
|
||||
viewport_y_ = y;
|
||||
viewport_w_ = w;
|
||||
viewport_h_ = h;
|
||||
// Si estamos en medio de un frame, aplicar inmediatamente.
|
||||
if (render_pass_ != nullptr) {
|
||||
applyViewport();
|
||||
}
|
||||
// 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) {
|
||||
@@ -102,19 +181,18 @@ void GpuFrameRenderer::setVSync(bool enabled) {
|
||||
const SDL_GPUPresentMode MODE = enabled
|
||||
? SDL_GPU_PRESENTMODE_VSYNC
|
||||
: SDL_GPU_PRESENTMODE_IMMEDIATE;
|
||||
// Composition por defecto: SDR sin HDR.
|
||||
if (!SDL_SetGPUSwapchainParameters(dev, device_.window(),
|
||||
SDL_GPU_SWAPCHAINCOMPOSITION_SDR, MODE)) {
|
||||
std::cerr << "[GpuFrameRenderer] SDL_SetGPUSwapchainParameters: " << SDL_GetError() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::applyViewport() {
|
||||
void GpuFrameRenderer::applyFinalViewport() {
|
||||
if (render_pass_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (viewport_w_ <= 0.0F || viewport_h_ <= 0.0F) {
|
||||
return; // full window por defecto, no setear nada
|
||||
return;
|
||||
}
|
||||
SDL_GPUViewport vp{};
|
||||
vp.x = viewport_x_;
|
||||
@@ -128,29 +206,24 @@ void GpuFrameRenderer::applyViewport() {
|
||||
|
||||
void GpuFrameRenderer::pushLine(float x1, float y1, float x2, float y2, float thickness,
|
||||
float r, float g, float b, float a) {
|
||||
// Extrusión perpendicular en CPU: por cada línea generamos 4 vértices que
|
||||
// forman un quad (2 triángulos = 6 índices).
|
||||
const float DX = x2 - x1;
|
||||
const float DY = y2 - y1;
|
||||
const float LEN = std::sqrt((DX * DX) + (DY * DY));
|
||||
if (LEN < 1e-6F) {
|
||||
return; // línea degenerada, saltar
|
||||
return;
|
||||
}
|
||||
|
||||
// Vector unitario perpendicular (90° CCW): (-DY, DX) / LEN.
|
||||
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());
|
||||
|
||||
// 4 vértices del quad: top-start, bottom-start, top-end, bottom-end.
|
||||
vertices_.push_back({x1 + NX, y1 + NY, r, g, b, a}); // 0: top-start
|
||||
vertices_.push_back({x1 - NX, y1 - NY, r, g, b, a}); // 1: bottom-start
|
||||
vertices_.push_back({x2 + NX, y2 + NY, r, g, b, a}); // 2: top-end
|
||||
vertices_.push_back({x2 - NX, y2 - NY, r, g, b, a}); // 3: bottom-end
|
||||
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});
|
||||
|
||||
// 2 triángulos: 0-1-2 y 1-3-2 (CCW)
|
||||
indices_.push_back(BASE_INDEX + 0);
|
||||
indices_.push_back(BASE_INDEX + 1);
|
||||
indices_.push_back(BASE_INDEX + 2);
|
||||
@@ -166,7 +239,6 @@ void GpuFrameRenderer::flushBatch() {
|
||||
|
||||
SDL_GPUDevice* dev = device_.get();
|
||||
|
||||
// Crear buffers transitorios para este frame.
|
||||
const uint32_t VBO_SIZE = static_cast<uint32_t>(vertices_.size() * sizeof(LineVertex));
|
||||
const uint32_t IBO_SIZE = static_cast<uint32_t>(indices_.size() * sizeof(uint16_t));
|
||||
|
||||
@@ -180,7 +252,6 @@ void GpuFrameRenderer::flushBatch() {
|
||||
ibo_info.size = IBO_SIZE;
|
||||
SDL_GPUBuffer* ibo = SDL_CreateGPUBuffer(dev, &ibo_info);
|
||||
|
||||
// Transfer buffer combinado (vertex + index) para subir los datos.
|
||||
SDL_GPUTransferBufferCreateInfo tbo_info{};
|
||||
tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
tbo_info.size = VBO_SIZE + IBO_SIZE;
|
||||
@@ -191,8 +262,7 @@ void GpuFrameRenderer::flushBatch() {
|
||||
std::memcpy(mapped + VBO_SIZE, indices_.data(), IBO_SIZE);
|
||||
SDL_UnmapGPUTransferBuffer(dev, tbo);
|
||||
|
||||
// Copy pass: subir transfer buffer → device buffers.
|
||||
// Importante: el copy pass debe ejecutarse FUERA del render pass.
|
||||
// Copy pass FUERA del render pass.
|
||||
SDL_EndGPURenderPass(render_pass_);
|
||||
render_pass_ = nullptr;
|
||||
|
||||
@@ -205,17 +275,16 @@ void GpuFrameRenderer::flushBatch() {
|
||||
SDL_UploadToGPUBuffer(copy_pass, &ibo_src, &ibo_dst, false);
|
||||
SDL_EndGPUCopyPass(copy_pass);
|
||||
|
||||
// Reabrir render pass (load_op = LOAD para preservar lo ya pintado).
|
||||
// Reabrir render pass sobre OFFSCREEN (load_op=LOAD para preservar el clear).
|
||||
SDL_GPUColorTargetInfo color_target{};
|
||||
color_target.texture = swapchain_texture_;
|
||||
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);
|
||||
applyViewport();
|
||||
|
||||
// Bind pipeline + buffers + uniforms.
|
||||
SDL_BindGPUGraphicsPipeline(render_pass_, pipeline_.get());
|
||||
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,
|
||||
@@ -230,22 +299,95 @@ void GpuFrameRenderer::flushBatch() {
|
||||
|
||||
SDL_DrawGPUIndexedPrimitives(render_pass_,
|
||||
static_cast<uint32_t>(indices_.size()),
|
||||
1, // num_instances
|
||||
0, // first_index
|
||||
0, // vertex_offset
|
||||
0); // first_instance
|
||||
1, 0, 0, 0);
|
||||
|
||||
// Liberar buffers transitorios (SDL los retiene hasta que el cmd buffer termine).
|
||||
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_);
|
||||
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);
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::endFrame() {
|
||||
if (cmd_buffer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
flushBatch();
|
||||
compositePass();
|
||||
if (render_pass_ != nullptr) {
|
||||
SDL_EndGPURenderPass(render_pass_);
|
||||
render_pass_ = nullptr;
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
// gpu_frame_renderer.hpp - Renderer de alto nivel basado en SDL_GPU
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// API por frame:
|
||||
// 1. beginFrame(clear_color) — acquire swapchain + begin render pass
|
||||
// 2. pushLine(x1, y1, x2, y2, thickness, r, g, b, a) — encola la línea
|
||||
// 3. endFrame() — flush del batch + submit + presenta
|
||||
// Flujo por frame:
|
||||
// 1. beginFrame(clear_color)
|
||||
// → acquire swapchain + begin render pass sobre la textura OFFSCREEN
|
||||
// (clear a black; la swapchain se pinta después con el postpro).
|
||||
// 2. pushLine(...) — encola líneas (extrusión en CPU).
|
||||
// 3. endFrame()
|
||||
// → flush del batch en offscreen + pase de postpro (sample offscreen
|
||||
// → swapchain con bloom/flicker/background) + submit + presenta.
|
||||
//
|
||||
// Internamente: extruye cada línea como un quad (4 vértices, 6 índices) en CPU
|
||||
// y construye un vertex buffer único por frame. Un solo draw call por frame.
|
||||
// La oscilación de brillo y el fondo pulsante viven ahora en el shader de
|
||||
// postpro; el CPU solo le pasa los uniformes del struct PostFxParams.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -19,9 +23,32 @@
|
||||
|
||||
#include "core/rendering/gpu/gpu_device.hpp"
|
||||
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
|
||||
#include "core/rendering/gpu/gpu_postfx_pipeline.hpp"
|
||||
|
||||
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};
|
||||
|
||||
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};
|
||||
};
|
||||
|
||||
class GpuFrameRenderer {
|
||||
public:
|
||||
GpuFrameRenderer() = default;
|
||||
@@ -32,47 +59,63 @@ class GpuFrameRenderer {
|
||||
GpuFrameRenderer(GpuFrameRenderer&&) = delete;
|
||||
auto operator=(GpuFrameRenderer&&) -> GpuFrameRenderer& = delete;
|
||||
|
||||
// Crea device + pipeline. logical_w/h = tamaño en píxeles lógicos del
|
||||
// juego (1280×720) — se usa como transformación a NDC en el shader.
|
||||
// Crea device + pipeline + offscreen + sampler. logical_w/h = tamaño
|
||||
// en píxeles lógicos del juego (1280×720), usado como base del
|
||||
// offscreen y de la transformación a NDC del shader de líneas.
|
||||
[[nodiscard]] auto init(SDL_Window* window, float logical_w, float logical_h) -> bool;
|
||||
void destroy();
|
||||
|
||||
// beginFrame: adquiere swapchain, abre render pass, hace clear.
|
||||
// Devuelve false si no hay textura disponible (ventana minimizada).
|
||||
// beginFrame: adquiere swapchain, abre render pass sobre offscreen
|
||||
// con clear a negro. Devuelve false si no hay textura disponible.
|
||||
// Los argumentos clear_r/g/b se ignoran (compatibilidad de API: el
|
||||
// fondo lo dibuja el postpro).
|
||||
[[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);
|
||||
|
||||
// endFrame: sube el VBO, ejecuta el draw, cierra render pass y presenta.
|
||||
// endFrame: flush del batch de líneas → composite postpro → submit + presenta.
|
||||
void endFrame();
|
||||
|
||||
// Viewport en píxeles físicos de la swapchain. Si w<=0 o h<=0, se
|
||||
// usa el tamaño completo de la ventana (sin letterbox).
|
||||
// Viewport del PASE FINAL (postpro → swapchain) en píxeles físicos.
|
||||
// Implementa el letterbox: cualquier área fuera del viewport queda
|
||||
// como el clear color del pase de composite (negro). Si w<=0 o h<=0,
|
||||
// se usa el tamaño completo de la ventana.
|
||||
void setViewport(float x, float y, float w, float h);
|
||||
|
||||
// Activa/desactiva VSync. true = SDL_GPU_PRESENTMODE_VSYNC, false = IMMEDIATE.
|
||||
void setVSync(bool enabled);
|
||||
|
||||
// Acceso a internals (necesario para SDLManager y futuros sistemas).
|
||||
// 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; }
|
||||
[[nodiscard]] auto postfx() const -> const PostFxParams& { return postfx_params_; }
|
||||
|
||||
// Acceso a internals.
|
||||
[[nodiscard]] auto device() -> GpuDevice& { return device_; }
|
||||
[[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; }
|
||||
|
||||
private:
|
||||
GpuDevice device_;
|
||||
GpuLinePipeline pipeline_;
|
||||
GpuLinePipeline line_pipeline_;
|
||||
GpuPostFxPipeline postfx_pipeline_;
|
||||
|
||||
// Tamaño lógico del juego (para transformación NDC en el shader).
|
||||
// Tamaño lógico del juego (= tamaño del offscreen).
|
||||
float logical_w_{1280.0F};
|
||||
float logical_h_{720.0F};
|
||||
|
||||
// Viewport en píxeles físicos. <0 = full window.
|
||||
// Viewport del pase final en píxeles físicos. <0 = full window.
|
||||
float viewport_x_{0.0F};
|
||||
float viewport_y_{0.0F};
|
||||
float viewport_w_{-1.0F};
|
||||
float viewport_h_{-1.0F};
|
||||
|
||||
// Offscreen color target (formato fijo R8G8B8A8_UNORM para portabilidad).
|
||||
SDL_GPUTexture* offscreen_texture_{nullptr};
|
||||
SDL_GPUTextureFormat offscreen_format_{SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM};
|
||||
SDL_GPUSampler* linear_sampler_{nullptr};
|
||||
|
||||
// Batch del frame en curso.
|
||||
std::vector<LineVertex> vertices_;
|
||||
std::vector<uint16_t> indices_;
|
||||
@@ -82,9 +125,15 @@ class GpuFrameRenderer {
|
||||
SDL_GPUTexture* swapchain_texture_{nullptr};
|
||||
SDL_GPURenderPass* render_pass_{nullptr};
|
||||
|
||||
// Parámetros del postpro (configurables vía YAML).
|
||||
PostFxParams postfx_params_{};
|
||||
|
||||
// Helpers internos.
|
||||
[[nodiscard]] auto createOffscreen() -> bool;
|
||||
void destroyOffscreen();
|
||||
void flushBatch();
|
||||
void applyViewport();
|
||||
void compositePass();
|
||||
void applyFinalViewport();
|
||||
};
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace Rendering::GPU {
|
||||
|
||||
GpuLinePipeline::~GpuLinePipeline() { destroy(); }
|
||||
|
||||
auto GpuLinePipeline::init(const GpuDevice& device) -> bool {
|
||||
auto GpuLinePipeline::init(const GpuDevice& device,
|
||||
SDL_GPUTextureFormat target_format) -> bool {
|
||||
owner_ = device.get();
|
||||
if (owner_ == nullptr) {
|
||||
return false;
|
||||
@@ -59,9 +60,10 @@ auto GpuLinePipeline::init(const GpuDevice& device) -> bool {
|
||||
vertex_input.vertex_attributes = attrs;
|
||||
vertex_input.num_vertex_attributes = 2;
|
||||
|
||||
// Color target = swapchain. Blending alpha estándar.
|
||||
// Color target = formato pasado por el caller (offscreen u otro).
|
||||
// Blending alpha estándar.
|
||||
SDL_GPUColorTargetDescription color_target{};
|
||||
color_target.format = device.swapchainFormat();
|
||||
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;
|
||||
|
||||
@@ -43,7 +43,11 @@ class GpuLinePipeline {
|
||||
GpuLinePipeline(GpuLinePipeline&&) = delete;
|
||||
auto operator=(GpuLinePipeline&&) -> GpuLinePipeline& = delete;
|
||||
|
||||
[[nodiscard]] auto init(const GpuDevice& device) -> bool;
|
||||
// target_format: formato del color target sobre el que renderizamos
|
||||
// (swapchain o offscreen). Por defecto coincide con el del swapchain
|
||||
// del device.
|
||||
[[nodiscard]] auto init(const GpuDevice& device,
|
||||
SDL_GPUTextureFormat target_format) -> bool;
|
||||
void destroy();
|
||||
|
||||
[[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; }
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// gpu_postfx_pipeline.cpp - Implementación del pipeline de postprocesado.
|
||||
|
||||
#include "core/rendering/gpu/gpu_postfx_pipeline.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "core/rendering/gpu/gpu_device.hpp"
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
GpuPostFxPipeline::~GpuPostFxPipeline() { destroy(); }
|
||||
|
||||
auto GpuPostFxPipeline::init(const GpuDevice& device,
|
||||
SDL_GPUTextureFormat target_format) -> bool {
|
||||
owner_ = device.get();
|
||||
if (owner_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// El vertex shader no usa UBO (emite tres vértices hardcodeados).
|
||||
// El fragment shader usa 1 sampler (escena) y 1 UBO (parámetros postpro).
|
||||
SDL_GPUShader* vert = device.loadShader("postfx.vert.spv",
|
||||
SDL_GPU_SHADERSTAGE_VERTEX,
|
||||
/*num_uniform_buffers=*/0,
|
||||
/*num_samplers=*/0);
|
||||
SDL_GPUShader* frag = device.loadShader("postfx.frag.spv",
|
||||
SDL_GPU_SHADERSTAGE_FRAGMENT,
|
||||
/*num_uniform_buffers=*/1,
|
||||
/*num_samplers=*/1);
|
||||
if ((vert == nullptr) || (frag == nullptr)) {
|
||||
if (vert != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
}
|
||||
if (frag != nullptr) {
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
}
|
||||
std::cerr << "[GpuPostFxPipeline] Error cargando shaders postfx\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sin vertex input: los tres vértices del triángulo se generan en el shader.
|
||||
SDL_GPUVertexInputState vertex_input{};
|
||||
vertex_input.vertex_buffer_descriptions = nullptr;
|
||||
vertex_input.num_vertex_buffers = 0;
|
||||
vertex_input.vertex_attributes = nullptr;
|
||||
vertex_input.num_vertex_attributes = 0;
|
||||
|
||||
// Color target del postpro = swapchain. Sin blending: el postpro reescribe
|
||||
// píxeles directamente (la mezcla con la escena ya se hizo dentro del shader).
|
||||
SDL_GPUColorTargetDescription color_target{};
|
||||
color_target.format = target_format;
|
||||
color_target.blend_state.enable_blend = false;
|
||||
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;
|
||||
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);
|
||||
|
||||
SDL_ReleaseGPUShader(owner_, vert);
|
||||
SDL_ReleaseGPUShader(owner_, frag);
|
||||
|
||||
if (pipeline_ == nullptr) {
|
||||
std::cerr << "[GpuPostFxPipeline] SDL_CreateGPUGraphicsPipeline: "
|
||||
<< SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuPostFxPipeline::destroy() {
|
||||
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
|
||||
}
|
||||
pipeline_ = nullptr;
|
||||
owner_ = nullptr;
|
||||
}
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
@@ -0,0 +1,72 @@
|
||||
// gpu_postfx_pipeline.hpp - Pipeline de postprocesado (fullscreen triangle)
|
||||
// © 2025 Orni Attack
|
||||
//
|
||||
// Pase final del frame: muestrea la escena renderizada en offscreen y aplica
|
||||
// bloom + flicker + background pulse en el fragment shader. El vertex shader
|
||||
// emite un único triángulo que cubre toda la pantalla, así que el draw no
|
||||
// necesita vertex buffer (DrawPrimitives con vertex_count=3).
|
||||
//
|
||||
// Recursos del shader (SDL_gpu set bindings):
|
||||
// fragment set=2, binding=0 → sampler2D (escena offscreen)
|
||||
// fragment set=3, binding=0 → uniform buffer (parámetros del postpro)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
namespace Rendering::GPU {
|
||||
|
||||
class GpuDevice;
|
||||
|
||||
// Uniform buffer del postpro. Debe coincidir EXACTAMENTE con
|
||||
// shaders/postfx.frag.glsl (layout std140 con vec4 alineadas a 16 bytes).
|
||||
struct PostFxUniforms {
|
||||
float time; // Tiempo acumulado en segundos
|
||||
float bloom_intensity; // Mezcla bloom (0..2)
|
||||
float bloom_threshold; // Luminancia mínima high-pass (0..1)
|
||||
float bloom_radius_px; // Radio del kernel en píxeles lógicos
|
||||
|
||||
float flicker_amplitude; // Profundidad del flicker (0..1)
|
||||
float flicker_frequency_hz; // Hz
|
||||
float background_pulse_freq_hz; // Hz
|
||||
float pad_a_;
|
||||
|
||||
float background_min_r; // Color min RGB en [0..1], A=1
|
||||
float background_min_g;
|
||||
float background_min_b;
|
||||
float background_min_a;
|
||||
|
||||
float background_max_r;
|
||||
float background_max_g;
|
||||
float background_max_b;
|
||||
float background_max_a;
|
||||
|
||||
float texel_size_x; // 1.0 / texture_width
|
||||
float texel_size_y;
|
||||
float pad_b_;
|
||||
float pad_c_;
|
||||
};
|
||||
|
||||
class GpuPostFxPipeline {
|
||||
public:
|
||||
GpuPostFxPipeline() = default;
|
||||
~GpuPostFxPipeline();
|
||||
|
||||
GpuPostFxPipeline(const GpuPostFxPipeline&) = delete;
|
||||
auto operator=(const GpuPostFxPipeline&) -> GpuPostFxPipeline& = delete;
|
||||
GpuPostFxPipeline(GpuPostFxPipeline&&) = delete;
|
||||
auto operator=(GpuPostFxPipeline&&) -> GpuPostFxPipeline& = delete;
|
||||
|
||||
// target_format: formato del color target del pase final (swapchain).
|
||||
[[nodiscard]] auto init(const GpuDevice& device,
|
||||
SDL_GPUTextureFormat target_format) -> bool;
|
||||
void destroy();
|
||||
|
||||
[[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; }
|
||||
|
||||
private:
|
||||
SDL_GPUDevice* owner_{nullptr};
|
||||
SDL_GPUGraphicsPipeline* pipeline_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
Reference in New Issue
Block a user