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:
2026-05-20 08:52:03 +02:00
parent 6d7060ceb5
commit a7aecbadd1
19 changed files with 731 additions and 198 deletions
+34
View File
@@ -0,0 +1,34 @@
# postfx.yaml - Parámetros del shader de postprocesado
#
# Este archivo configura el pase final del renderer (bloom + flicker +
# background pulse). Se carga al iniciar el juego desde resources.pack.
# Si falta o tiene errores, se usan los valores por defecto de
# Defaults::PostFx (defaults.hpp).
#
# Tip de tuning:
# - Para más "neón vector", sube bloom.intensity y bloom.radius_px.
# - Para más "CRT viejo", sube flicker.amplitude (riesgo de mareo si >0.3).
# - Background es muy sutil; pasa los componentes G a 0.15-0.20 para
# un fondo verde-tenue más marcado.
# Bloom / glow: desenfoque gaussiano de las regiones brillantes.
bloom:
enabled: true
intensity: 0.6 # 0..2 — cuanto del bloom se suma a la imagen
threshold: 0.30 # 0..1 — luminancia mínima que aporta al bloom
radius_px: 2.0 # radio del kernel en píxeles lógicos (1..8 razonable)
# Flicker: modulación global de brillo (efecto fósforo CRT).
# Sustituye a la antigua oscilación CPU del ColorOscillator.
flicker:
enabled: true
amplitude: 0.10 # 0..1 — profundidad del flicker
frequency_hz: 6.0 # Hz — velocidad de la pulsación
# Background pulse: color de fondo oscilante (suma aditiva).
# RGB en [0..255]; el shader normaliza a [0..1].
background:
enabled: true
color_min: [0, 5, 0] # negro casi puro
color_max: [0, 15, 0] # verde muy tenue
pulse_frequency_hz: 6.0 # Hz — sincronizado con flicker por defecto
+82
View File
@@ -0,0 +1,82 @@
#version 450
// Fragment shader del pase de postprocesado.
// Lee la textura offscreen (escena vectorial sobre fondo negro) y produce
// el fragmento final aplicando:
// 1. Bloom kernel 5×5 con high-pass (solo los brillos por encima de
// threshold contribuyen).
// 2. Flicker: multiplicador global de brillo modulado por tiempo
// (sustituye al oscilador CPU del legacy).
// 3. Background pulse: color de fondo que oscila entre min y max y se
// suma a la imagen (las líneas brillan por encima).
//
// Resource sets (SDL_gpu):
// set=2, binding=0 → sampler2D (escena offscreen)
// set=3, binding=0 → uniform buffer (parámetros del postpro)
layout(set = 2, binding = 0) uniform sampler2D scene;
layout(set = 3, binding = 0) uniform PostFxUBO {
float time;
float bloom_intensity;
float bloom_threshold;
float bloom_radius_px;
float flicker_amplitude;
float flicker_frequency_hz;
float background_pulse_freq_hz;
float _pad_a;
vec4 background_min; // RGB en [0..1], A=1
vec4 background_max; // RGB en [0..1], A=1
vec2 texel_size; // 1.0 / texture_size
vec2 _pad_b;
} ubo;
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 frag;
const float TAU = 6.28318530718;
void main() {
// === BLOOM ===
// Kernel 5×5 con muestreo radial y high-pass por luminancia (max RGB).
// Pesos gaussianos: w = exp(-(dx²+dy²) / 4).
vec3 src = texture(scene, v_uv).rgb;
vec3 bloom = vec3(0.0);
float total_weight = 0.0;
for (int dy = -2; dy <= 2; ++dy) {
for (int dx = -2; dx <= 2; ++dx) {
vec2 offset = vec2(float(dx), float(dy)) * ubo.texel_size * ubo.bloom_radius_px;
vec3 c = texture(scene, v_uv + offset).rgb;
float luma = max(c.r, max(c.g, c.b));
float high_pass = max(0.0, luma - ubo.bloom_threshold);
float w = exp(-(float(dx * dx + dy * dy)) / 4.0);
bloom += c * high_pass * w;
total_weight += w;
}
}
if (total_weight > 0.0) {
bloom /= total_weight;
}
bloom *= ubo.bloom_intensity;
// === FLICKER ===
// Multiplicador global de brillo. Oscila entre (1.0 - amplitude) y 1.0.
// amplitude=0 → sin flicker; amplitude=1 → pulsa entre apagado y máximo.
float pulse = (sin(ubo.time * ubo.flicker_frequency_hz * TAU) * 0.5) + 0.5;
float flicker = 1.0 - (ubo.flicker_amplitude * (1.0 - pulse));
// === BACKGROUND PULSE ===
// Suma de color de fondo oscilante. min..max se interpolan con sin(t).
float bg_pulse = (sin(ubo.time * ubo.background_pulse_freq_hz * TAU) * 0.5) + 0.5;
vec3 background = mix(ubo.background_min.rgb, ubo.background_max.rgb, bg_pulse);
// === COMPOSICIÓN ===
// El offscreen viene con clear=black, por lo que solo las líneas y el
// bloom aportan luz. Sumamos el fondo y luego multiplicamos por flicker
// para que el pulso afecte a todo (líneas + bloom + bg).
vec3 lines_and_glow = (src + bloom) * flicker;
frag = vec4(background + lines_and_glow, 1.0);
}
+28
View File
@@ -0,0 +1,28 @@
#version 450
// Vertex shader del pase de postprocesado.
// Emite un solo triángulo que cubre toda la pantalla (técnica del "fullscreen
// triangle"): tres vértices en (-1,-1), (3,-1), (-1,3) → la región visible
// [-1..1]² queda completamente cubierta y el clip recorta el resto. No hace
// falta vertex buffer; el draw es DrawPrimitives(vertex_count=3).
layout(location = 0) out vec2 v_uv;
void main() {
vec2 positions[3] = vec2[3](
vec2(-1.0, -1.0),
vec2( 3.0, -1.0),
vec2(-1.0, 3.0)
);
// UV.y invertida para compensar la diferencia entre la convención de
// clip-space del line shader (ndc.y flipeado, GL-style) y la convención
// de muestreo de SDL_gpu/Vulkan (origen de textura en top-left). Sin esta
// inversión, el offscreen se ve cabeza-abajo en el composite.
vec2 uvs[3] = vec2[3](
vec2(0.0, 1.0),
vec2(2.0, 1.0),
vec2(0.0, -1.0)
);
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
v_uv = uvs[gl_VertexIndex];
}
+108
View File
@@ -0,0 +1,108 @@
// postfx_config.cpp - Implementación del cargador de YAML del postpro.
#include "core/config/postfx_config.hpp"
#include <iostream>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
namespace Config::PostFx {
namespace {
// Helper: lee `key` en `node` solo si existe; deja `dst` intacto en caso
// contrario. Así, un YAML parcial sigue funcionando con los defaults del
// struct para los campos que falten.
template <typename T>
void readField(const fkyaml::node& node, const char* key, T& dst) {
if (node.contains(key)) {
dst = node[key].get_value<T>();
}
}
// Lee un array RGB [r, g, b] (0..255) y lo normaliza a [0..1] sobre tres
// destinos floats. Si la clave no existe o no es secuencia de 3, deja los
// destinos como están.
void readRgb255(const fkyaml::node& node, const char* key,
float& dst_r, float& dst_g, float& dst_b) {
if (!node.contains(key)) {
return;
}
const auto& arr = node[key];
if (!arr.is_sequence() || arr.size() < 3) {
return;
}
try {
const auto R = arr[0].get_value<int>();
const auto G = arr[1].get_value<int>();
const auto B = arr[2].get_value<int>();
dst_r = static_cast<float>(R) / 255.0F;
dst_g = static_cast<float>(G) / 255.0F;
dst_b = static_cast<float>(B) / 255.0F;
} catch (...) {
// Mantiene los defaults si algún elemento no es entero.
}
}
} // namespace
auto load(const std::string& path) -> Rendering::GPU::PostFxParams {
Rendering::GPU::PostFxParams params{}; // valores por defecto del struct
auto bytes = Resource::Helper::loadFile(path);
if (bytes.empty()) {
std::cerr << "[PostFxConfig] No se pudo cargar " << path
<< " — usando defaults built-in\n";
return params;
}
try {
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (yaml.contains("bloom") && yaml["bloom"].is_mapping()) {
const auto& node = yaml["bloom"];
readField(node, "enabled", params.bloom_enabled);
readField(node, "intensity", params.bloom_intensity);
readField(node, "threshold", params.bloom_threshold);
readField(node, "radius_px", params.bloom_radius_px);
}
if (yaml.contains("flicker") && yaml["flicker"].is_mapping()) {
const auto& node = yaml["flicker"];
readField(node, "enabled", params.flicker_enabled);
readField(node, "amplitude", params.flicker_amplitude);
readField(node, "frequency_hz", params.flicker_frequency_hz);
}
if (yaml.contains("background") && yaml["background"].is_mapping()) {
const auto& node = yaml["background"];
readField(node, "enabled", params.background_enabled);
readRgb255(node, "color_min",
params.background_min_r,
params.background_min_g,
params.background_min_b);
readRgb255(node, "color_max",
params.background_max_r,
params.background_max_g,
params.background_max_b);
readField(node, "pulse_frequency_hz", params.background_pulse_freq_hz);
}
std::cout << "[PostFxConfig] Cargado " << path
<< " (bloom=" << (params.bloom_enabled ? "on" : "off")
<< " intensity=" << params.bloom_intensity
<< ", flicker=" << (params.flicker_enabled ? "on" : "off")
<< " amp=" << params.flicker_amplitude
<< ", bg=" << (params.background_enabled ? "on" : "off")
<< ")\n";
} catch (const fkyaml::exception& e) {
std::cerr << "[PostFxConfig] Error parseando " << path << ": " << e.what()
<< " — usando defaults built-in\n";
}
return params;
}
} // namespace Config::PostFx
+21
View File
@@ -0,0 +1,21 @@
// postfx_config.hpp - Carga de los parámetros del shader de postpro desde YAML.
// © 2025 Orni Attack
//
// Lee `config/postfx.yaml` (dentro de resources.pack) y devuelve un struct
// PostFxParams listo para pasar a GpuFrameRenderer::setPostFx(). Si el YAML
// no existe o falla el parser, retorna los defaults built-in.
#pragma once
#include <string>
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
namespace Config::PostFx {
// Carga desde el resource pack. Path relativo dentro del pack (p.ej.
// "config/postfx.yaml"). Si falla, devuelve un PostFxParams construido por
// defecto (valores embebidos en el struct).
[[nodiscard]] auto load(const std::string& path) -> Rendering::GPU::PostFxParams;
} // namespace Config::PostFx
+3 -23
View File
@@ -248,29 +248,9 @@ namespace Math {
constexpr float PI = std::numbers::pi_v<float>;
} // namespace Math
// Colores (oscilación para efecto CRT)
namespace Color {
// Frecuencia de oscilación
constexpr float FREQUENCY = 6.0F; // 1 Hz (1 ciclo/segundo)
// Color de líneas (efecto fósforo verde CRT)
constexpr uint8_t LINE_MIN_R = 100; // Verde oscuro
constexpr uint8_t LINE_MIN_G = 200;
constexpr uint8_t LINE_MIN_B = 100;
constexpr uint8_t LINE_MAX_R = 100; // Verde brillante
constexpr uint8_t LINE_MAX_G = 255;
constexpr uint8_t LINE_MAX_B = 100;
// Color de fondo (pulso sutil verde oscuro)
constexpr uint8_t BACKGROUND_MIN_R = 0; // Negro
constexpr uint8_t BACKGROUND_MIN_G = 5;
constexpr uint8_t BACKGROUND_MIN_B = 0;
constexpr uint8_t BACKGROUND_MAX_R = 0; // Verde muy oscuro
constexpr uint8_t BACKGROUND_MAX_G = 15;
constexpr uint8_t BACKGROUND_MAX_B = 0;
} // namespace Color
// La antigua oscilación CPU (namespace Color) se ha migrado al shader de
// postpro. Los parámetros de flicker / background pulse viven ahora en
// data/config/postfx.yaml y se aplican en shaders/postfx.frag.glsl.
// Brillantor (control de intensitat per cada type de entidad)
namespace Brightness {
@@ -1,68 +0,0 @@
// color_oscillator.cpp - Implementació de oscil·lació de color
// © 2025 Port a C++20 con SDL3
#include "core/rendering/color_oscillator.hpp"
#include <cmath>
#include "core/defaults.hpp"
namespace Rendering {
ColorOscillator::ColorOscillator()
: accumulated_time_(0.0F) {
// Inicialitzar con el color mínim
current_line_color_ = {.r = Defaults::Color::LINE_MIN_R,
.g = Defaults::Color::LINE_MIN_G,
.b = Defaults::Color::LINE_MIN_B,
.a = 255};
current_background_color_ = {.r = Defaults::Color::BACKGROUND_MIN_R,
.g = Defaults::Color::BACKGROUND_MIN_G,
.b = Defaults::Color::BACKGROUND_MIN_B,
.a = 255};
}
void ColorOscillator::update(float delta_time) {
accumulated_time_ += delta_time;
float factor =
calculateOscillationFactor(accumulated_time_, Defaults::Color::FREQUENCY);
// Interpolar colors de línies
SDL_Color line_min = {Defaults::Color::LINE_MIN_R,
Defaults::Color::LINE_MIN_G,
Defaults::Color::LINE_MIN_B,
255};
SDL_Color line_max = {Defaults::Color::LINE_MAX_R,
Defaults::Color::LINE_MAX_G,
Defaults::Color::LINE_MAX_B,
255};
current_line_color_ = interpolateColor(line_min, line_max, factor);
// Interpolar colors de fons
SDL_Color bg_min = {Defaults::Color::BACKGROUND_MIN_R,
Defaults::Color::BACKGROUND_MIN_G,
Defaults::Color::BACKGROUND_MIN_B,
255};
SDL_Color bg_max = {Defaults::Color::BACKGROUND_MAX_R,
Defaults::Color::BACKGROUND_MAX_G,
Defaults::Color::BACKGROUND_MAX_B,
255};
current_background_color_ = interpolateColor(bg_min, bg_max, factor);
}
float ColorOscillator::calculateOscillationFactor(float time, float frequency) {
// Oscil·lació senoïdal: sin(t * freq * 2π)
// Mapejar de [-1, 1] a [0, 1]
float radians = time * frequency * 2.0F * Defaults::Math::PI;
return (std::sin(radians) + 1.0F) / 2.0F;
}
SDL_Color ColorOscillator::interpolateColor(SDL_Color min, SDL_Color max, float factor) {
return {static_cast<uint8_t>(min.r + ((max.r - min.r) * factor)),
static_cast<uint8_t>(min.g + ((max.g - min.g) * factor)),
static_cast<uint8_t>(min.b + ((max.b - min.b) * factor)),
255};
}
} // namespace Rendering
@@ -1,29 +0,0 @@
// color_oscillator.hpp - Sistema de oscil·lació de color per efecte CRT
// © 2025 Port a C++20 con SDL3
#pragma once
#include <SDL3/SDL.h>
namespace Rendering {
class ColorOscillator {
public:
ColorOscillator();
void update(float delta_time);
[[nodiscard]] SDL_Color getCurrentLineColor() const { return current_line_color_; }
[[nodiscard]] SDL_Color getCurrentBackgroundColor() const {
return current_background_color_;
}
private:
float accumulated_time_;
SDL_Color current_line_color_;
SDL_Color current_background_color_;
static float calculateOscillationFactor(float time, float frequency);
static SDL_Color interpolateColor(SDL_Color min, SDL_Color max, float factor);
};
} // namespace Rendering
+3 -2
View File
@@ -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;
+3 -1
View File
@@ -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_; }
+180 -38
View File
@@ -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
+5 -3
View File
@@ -6,8 +6,10 @@
namespace Rendering {
// Color global compartido (actualizado por ColorOscillator via SDLManager).
SDL_Color g_current_line_color = {255, 255, 255, 255};
// Color global compartido para líneas sin paleta propia (HUD, debug, texto
// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro.
SDL_Color g_current_line_color = {100, 255, 100, 255};
// Grosor global por defecto. Configurable via setLineThickness.
// 1.5 da una línea visible y crujiente; 1.0 se ve demasiado fino en pantallas grandes.
@@ -29,7 +31,7 @@ void linea(Renderer* renderer,
const float FX2 = static_cast<float>(x2);
const float FY2 = static_cast<float>(y2);
// color.alpha==0 → usar global del oscilador. alpha>0 → color directo.
// color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo.
const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color;
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
+14 -8
View File
@@ -9,6 +9,7 @@
#include <format>
#include <iostream>
#include "core/config/postfx_config.hpp"
#include "core/defaults.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/coordinate_transform.hpp"
@@ -48,6 +49,12 @@ auto initWindowAndGpu(SDL_Window** out_window,
}
gpu_renderer.setVSync(Options::rendering.vsync != 0);
// Cargar parámetros del postpro desde el resource pack. Si el YAML falta
// o falla, el loader devuelve los defaults built-in (bloom suave + flicker
// sutil + background verde tenue).
gpu_renderer.setPostFx(Config::PostFx::load("config/postfx.yaml"));
*out_window = window;
return true;
}
@@ -314,15 +321,13 @@ auto SDLManager::handleWindowEvent(const SDL_Event& event) -> bool {
}
void SDLManager::clear(uint8_t r, uint8_t g, uint8_t b) {
// Usar el color oscilatorio de fondo (sustituye los parámetros pasados,
// que solo existían por compatibilidad con la API anterior).
// El fondo lo dibuja ahora el shader de postpro (background pulse). El
// offscreen se limpia en negro dentro de beginFrame. Los argumentos r/g/b
// se mantienen por compatibilidad de API.
(void)r;
(void)g;
(void)b;
SDL_Color bg = color_oscillator_.getCurrentBackgroundColor();
gpu_renderer_.beginFrame(static_cast<float>(bg.r) / 255.0F,
static_cast<float>(bg.g) / 255.0F,
static_cast<float>(bg.b) / 255.0F);
gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F);
}
void SDLManager::present() {
@@ -330,8 +335,9 @@ void SDLManager::present() {
}
void SDLManager::updateColors(float delta_time) {
color_oscillator_.update(delta_time);
Rendering::setLineColor(color_oscillator_.getCurrentLineColor());
// No-op desde la migración a postpro. La oscilación de brillo y el pulso
// de fondo los aplica shaders/postfx.frag.glsl directamente sobre wall-clock.
(void)delta_time;
}
void SDLManager::updateFPS(float delta_time) {
+3 -4
View File
@@ -13,7 +13,6 @@
#include <cstdint>
#include <string>
#include "core/rendering/color_oscillator.hpp"
#include "core/rendering/render_context.hpp"
class SDLManager {
@@ -37,7 +36,9 @@ class SDLManager {
void clear(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0);
void present();
// [NUEVO] Actualització de colors (oscil·lació)
// No-op desde la migración a postpro (la oscilación de brillo la
// gestiona el shader, no la CPU). Se mantiene la firma para no tocar
// los escenarios que la siguen invocando.
void updateColors(float delta_time);
// [NUEVO] Actualitzar counter de FPS
@@ -82,6 +83,4 @@ class SDLManager {
void applyWindowSize(int width, int height); // Canviar mida + centrar
void updateViewport(); // Configurar viewport con letterbox
// [NUEVO] Oscil·lador de colors
Rendering::ColorOscillator color_oscillator_;
};