Fase 7a: infraestructura SDL3 GPU (dormida, sin tocar runtime)

Preparacion del pipeline GPU. Codigo nuevo aislado en
core/rendering/gpu/; el runtime sigue usando SDL_Renderer hasta
Fase 7b. Tras 7a el juego sigue funcionando identico.

Shaders (shaders/):
- line.vert.glsl: vertex shader, transforma de pixeles logicos a NDC
  via uniform buffer LineUniforms{viewport_w, viewport_h}.
- line.frag.glsl: pinta el color RGBA interpolado.

Build:
- CMakeLists.txt: step nuevo que compila *.glsl a build/shaders/*.spv
  con glslc. ALL depende del target 'shaders' para incluirlo en cada
  build. Falla en cmake config si glslc no esta instalado.

Wrappers C++ (source/core/rendering/gpu/):
- gpu_device.hpp/cpp: GpuDevice, claim del window, loadShader desde
  .spv. Backends solicitados: Vulkan + Metal (sin DirectX).
- gpu_line_pipeline.hpp/cpp: GpuLinePipeline. Vertex layout
  (vec2 pos + vec4 color), primitive TRIANGLELIST (lineas como
  quads), alpha blending estandar, sin culling ni depth.
- gpu_frame_renderer.hpp/cpp: GpuFrameRenderer, API alto nivel:
  beginFrame / pushLine / endFrame. Extrusion perpendicular en CPU
  por linea (thickness libre por linea). Un draw call por frame
  con vertex+index buffers transitorios.

Plan: 7b swap del SDL_Renderer al GpuFrameRenderer en SDLManager.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 14:01:34 +02:00
parent 9993b2d98c
commit ba6fd00b54
10 changed files with 733 additions and 21 deletions
+40
View File
@@ -135,6 +135,46 @@ add_custom_command(
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V ---
# Compila tots els shaders .glsl a SPIR-V (Vulkan/Linux/Windows).
# macOS necessitarà MSL en el futur (Metal) — es generen amb spirv-cross
# o glslang amb target distint en una etapa posterior.
# Sortida: build/shaders/*.spv
find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
if(GLSLC_EXE)
file(GLOB SHADER_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/shaders/*.glsl")
set(COMPILED_SHADERS "")
foreach(SHADER ${SHADER_SOURCES})
get_filename_component(SHADER_NAME ${SHADER} NAME)
# Detectar stage del nom: line.vert.glsl → vert, line.frag.glsl → frag
if(SHADER_NAME MATCHES "\\.vert\\.glsl$")
set(SHADER_STAGE "vert")
string(REPLACE ".glsl" ".spv" SPV_NAME ${SHADER_NAME})
elseif(SHADER_NAME MATCHES "\\.frag\\.glsl$")
set(SHADER_STAGE "frag")
string(REPLACE ".glsl" ".spv" SPV_NAME ${SHADER_NAME})
else()
message(WARNING "Shader sense stage detectat: ${SHADER_NAME} (esperat .vert.glsl o .frag.glsl)")
continue()
endif()
set(SPV_OUTPUT "${CMAKE_BINARY_DIR}/shaders/${SPV_NAME}")
add_custom_command(
OUTPUT ${SPV_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/shaders"
COMMAND ${GLSLC_EXE} -fshader-stage=${SHADER_STAGE} -O ${SHADER} -o ${SPV_OUTPUT}
DEPENDS ${SHADER}
COMMENT "Compilant shader ${SHADER_NAME} → ${SPV_NAME}"
VERBATIM
)
list(APPEND COMPILED_SHADERS ${SPV_OUTPUT})
endforeach()
add_custom_target(shaders ALL DEPENDS ${COMPILED_SHADERS})
add_dependencies(${PROJECT_NAME} shaders)
message(STATUS "Shaders trobats: ${SHADER_SOURCES}")
else()
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V")
endif()
# --- STATIC ANALYSIS / FORMAT TARGETS ---
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
find_program(CLANG_FORMAT_EXE NAMES clang-format)
+31 -21
View File
@@ -36,36 +36,46 @@ Tag de seguridad: **`beta-3.0`** (snapshot de `main` antes de empezar).
| 6a+b — `body_` en Entity + `world_` en GameScene | ✅ | `0574077` |
| 6c — Migrar Ship | ✅ | `2fe22ff` |
| 6d — Migrar Enemy | ✅ | `27242f5` |
| **6e — Migrar Bullet** | ✅ | (este commit) |
| 7 — Migración a SDL3 GPU (sin fallback) | 🔲 **siguiente** | — |
| 6e — Migrar Bullet | ✅ | `9993b2d` |
| **7a — Infra GPU (shaders + wrappers, runtime dormido)** | ✅ | (este commit) |
| 7b — Swap SDL_Renderer → SDL_GPUDevice (clear/present por GPU) | 🔲 | — |
| 7c — Pipeline de líneas (quads con thickness configurable) | 🔲 | — |
| 7d — Refactor de firmas (SDL_Renderer* → FrameContext*) | 🔲 | — |
| 7e — Cleanup + smoke test final | 🔲 | — |
| 8 — Postprocesado, color, paleta por tipo | 🔲 | — |
| 9 — Refactor de GameScene (2.877 LOC → módulos) | 🔲 | — |
| 10 — Tuning final de masa/restitución/damping | 🔲 | — |
## Lo que queda inmediato (Fase 7)
## Lo que queda inmediato (Fase 7b)
**Migración del renderizado a SDL3 GPU sin fallback.**
**Swap del runtime de SDL_Renderer a SDL_GPUDevice.**
Fase 6e completada: las balas son cinemáticas dentro del PhysicsWorld (radius=0
en el world → no colisionan físicamente, solo gameplay-level via check_collision).
Body con mass=0.5, restitution=0, damping=0. `disparar()` setea
`body_.position` + `body_.velocity` cartesiana; `update()` detecta salida de
PLAYAREA y desactiva; `postUpdate()` sincroniza `center_` desde body. Smoke
test xvfb pasa sin errores.
Fase 7a completada: infraestructura GPU dormida (no llamada en runtime aún).
- `shaders/line.{vert,frag}.glsl` → compilados en build a `build/shaders/*.spv`
(CMake step con `glslc`). Vertex layout: vec2 position + vec4 color.
Transformación a NDC vía uniform buffer `LineUniforms{viewport_w, viewport_h}`.
- `core/rendering/gpu/`:
- `GpuDevice`: claim del window, loadShader desde .spv.
- `GpuLinePipeline`: pipeline TRIANGLELIST con vertex layout + alpha blending.
- `GpuFrameRenderer`: API `beginFrame / pushLine / endFrame`.
Extrusión perpendicular en CPU por línea (thickness configurable libre).
Un draw call por frame con un vertex/index buffer transitorio.
Decisión técnica del proyecto: **no fallback a SDL_Renderer**
([memoria](./.claude/projects/-home-sergio-gitea-orni-attack/memory/project_no_sdl_fallback.md)).
La fase 7 reemplaza completamente el pipeline actual de SDL_Renderer
(`SDL_RenderLine`, `SDL_RenderGeometry`) por SDL3 GPU.
Backend: Vulkan (Linux/Windows) y Metal (macOS). Sin DirectX.
Tareas previstas Fase 7:
- Sustituir `SDL_Renderer*` por `SDL_GPUDevice*` en SDLManager.
- Setup de swapchain + shaders básicos (vertex + fragment) para líneas vectoriales.
- Migrar `Rendering::render_shape` a un pipeline GPU que reciba vertex buffers.
- Postprocesado mínimo (Fase 8): bloom/glow, paleta por tipo de entidad.
Fase 7b siguiente:
1. En `SDLManager::iniciar`, sustituir `SDL_CreateRenderer` por
`Rendering::GPU::GpuFrameRenderer::init(window, 1280, 720)`.
2. `SDLManager::neteja(r,g,b)``gpu_renderer_.beginFrame(r/255, g/255, b/255)`.
3. `SDLManager::presenta()``gpu_renderer_.endFrame()`.
4. Borrar `renderer_` (SDL_Renderer*) del SDLManager.
5. Validar arranque con xvfb: pantalla negra (sin líneas todavía), sin crash.
**Fase 10 (tuning)** queda pendiente para después de SDL3 GPU + postpro. El usuario
prefiere terminar la migración técnica antes de tunear feel.
Después de 7b el juego se verá NEGRO porque ningún sistema sabe pintar líneas
todavía. Eso se arregla en 7c (line_renderer apunta al pipeline GPU) y 7d
(refactor de firmas SDL_Renderer* → FrameContext*).
**Fase 10 (tuning)** queda pendiente para después de SDL3 GPU + postpro.
## Memoria del proyecto
+12
View File
@@ -0,0 +1,12 @@
#version 450
// Fragment shader para líneas vectoriales.
// Pinta el color interpolado per-vertex que viene del vertex shader.
// (Postprocesado / bloom / glow son responsabilidad de la Fase 8.)
layout(location = 0) in vec4 frag_color;
layout(location = 0) out vec4 out_color;
void main() {
out_color = frag_color;
}
+29
View File
@@ -0,0 +1,29 @@
#version 450
// Vertex shader para líneas vectoriales.
// Las líneas se proveen ya extrudidas en CPU como quads (2 triángulos por línea)
// con grosor configurable. El vertex shader solo:
// 1. Transforma de píxeles lógicos (0..viewport_size) a clip-space (-1..+1).
// 2. Pasa el color RGBA al fragment shader.
//
// Slot de uniform buffer 0 (vertex): viewport size para la transformación.
// Convención SDL_gpu: SDL_PushGPUVertexUniformData(cmd, 0, &ubo, sizeof(ubo)).
layout(set = 1, binding = 0) uniform UBO {
vec2 viewport_size; // ancho y alto en píxeles lógicos (ej. 1280, 720)
vec2 _padding; // alineamiento a 16 bytes
} ubo;
layout(location = 0) in vec2 in_position; // píxeles lógicos
layout(location = 1) in vec4 in_color; // RGBA 0..1
layout(location = 0) out vec4 frag_color;
void main() {
// Píxeles lógicos -> NDC (-1..+1)
vec2 ndc = (in_position / ubo.viewport_size) * 2.0 - 1.0;
// Y flip: SDL screen-Y va hacia abajo, clip-Y hacia arriba.
ndc.y = -ndc.y;
gl_Position = vec4(ndc, 0.0, 1.0);
frag_color = in_color;
}
+104
View File
@@ -0,0 +1,104 @@
// gpu_device.cpp - Implementación del wrapper de SDL_GPUDevice
#include "core/rendering/gpu/gpu_device.hpp"
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include "core/utils/path_utils.hpp"
namespace Rendering::GPU {
GpuDevice::~GpuDevice() { destroy(); }
auto GpuDevice::init(SDL_Window* window) -> bool {
window_ = window;
// Solicitar backends en orden de preferencia: Vulkan (Linux/Windows),
// Metal (macOS). Sin DirectX según decisión de proyecto.
// SDL_GPU_SHADERFORMAT_SPIRV: shaders compilados con glslc.
// SDL_GPU_SHADERFORMAT_MSL: pendiente para macOS (Fase futura).
constexpr SDL_GPUShaderFormat SHADER_FORMATS =
SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL;
device_ = SDL_CreateGPUDevice(SHADER_FORMATS, /*debug=*/true, /*name=*/nullptr);
if (device_ == nullptr) {
std::cerr << "[GpuDevice] SDL_CreateGPUDevice falló: " << SDL_GetError() << '\n';
return false;
}
const char* driver = SDL_GetGPUDeviceDriver(device_);
std::cout << "[GpuDevice] Backend GPU: " << (driver != nullptr ? driver : "?") << '\n';
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
std::cerr << "[GpuDevice] SDL_ClaimWindowForGPUDevice falló: " << SDL_GetError() << '\n';
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
return false;
}
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
return true;
}
void GpuDevice::destroy() {
if (device_ != nullptr) {
if (window_ != nullptr) {
SDL_ReleaseWindowFromGPUDevice(device_, window_);
}
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
}
window_ = nullptr;
swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
}
auto GpuDevice::loadShader(const std::string& spv_filename,
SDL_GPUShaderStage stage,
uint32_t num_uniform_buffers) const -> SDL_GPUShader* {
if (device_ == nullptr) {
return nullptr;
}
// Los .spv viven en build/shaders/ junto al binario.
const std::string PATH = Utils::getExecutableDirectory() + "/shaders/" + spv_filename;
std::ifstream file(PATH, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
std::cerr << "[GpuDevice] No s'ha pogut obrir el shader: " << PATH << '\n';
return nullptr;
}
const std::streamsize SIZE = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(static_cast<size_t>(SIZE));
if (!file.read(reinterpret_cast<char*>(buffer.data()), SIZE)) {
std::cerr << "[GpuDevice] Error llegint shader: " << PATH << '\n';
return nullptr;
}
SDL_GPUShaderCreateInfo info{};
info.code = buffer.data();
info.code_size = buffer.size();
info.entrypoint = "main";
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
info.stage = stage;
info.num_uniform_buffers = num_uniform_buffers;
info.num_samplers = 0;
info.num_storage_buffers = 0;
info.num_storage_textures = 0;
SDL_GPUShader* shader = SDL_CreateGPUShader(device_, &info);
if (shader == nullptr) {
std::cerr << "[GpuDevice] SDL_CreateGPUShader (" << spv_filename << "): " << SDL_GetError() << '\n';
}
return shader;
}
} // namespace Rendering::GPU
+57
View File
@@ -0,0 +1,57 @@
// gpu_device.hpp - Wrapper de SDL_GPUDevice
// © 2025 Orni Attack
//
// Ownership del SDL_GPUDevice y del claim del window. Backend preferido:
// Vulkan (Linux, Windows) y Metal (macOS). Sin DirectX.
//
// Uso:
// GpuDevice device;
// if (!device.init(window)) return -1; // claim del window
// ... renderer setup ...
// device.destroy(); // unclaim + destroy device
#pragma once
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <string>
namespace Rendering::GPU {
class GpuDevice {
public:
GpuDevice() = default;
~GpuDevice();
// No copia / move (RAII propietario del device).
GpuDevice(const GpuDevice&) = delete;
auto operator=(const GpuDevice&) -> GpuDevice& = delete;
GpuDevice(GpuDevice&&) = delete;
auto operator=(GpuDevice&&) -> GpuDevice& = delete;
// Crea el device y claim el window. Devuelve false si no hay backend
// soportado o si el driver no permite el claim.
[[nodiscard]] auto init(SDL_Window* window) -> bool;
void destroy();
// Carga un shader SPIR-V desde build/shaders/{name}.spv. Devuelve un
// SDL_GPUShader* del que ahora es responsable el caller (libera con
// SDL_ReleaseGPUShader). Retorna nullptr si falla.
//
// num_uniform_buffers: nº de uniform buffers que usa el shader (slot 0..N-1).
[[nodiscard]] auto loadShader(const std::string& spv_filename,
SDL_GPUShaderStage stage,
uint32_t num_uniform_buffers) const -> SDL_GPUShader*;
[[nodiscard]] auto get() const -> SDL_GPUDevice* { return device_; }
[[nodiscard]] auto window() const -> SDL_Window* { return window_; }
[[nodiscard]] auto swapchainFormat() const -> SDL_GPUTextureFormat { return swapchain_format_; }
private:
SDL_GPUDevice* device_{nullptr};
SDL_Window* window_{nullptr};
SDL_GPUTextureFormat swapchain_format_{SDL_GPU_TEXTUREFORMAT_INVALID};
};
} // namespace Rendering::GPU
@@ -0,0 +1,213 @@
// gpu_frame_renderer.cpp - Implementación del FrameRenderer
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>
namespace Rendering::GPU {
GpuFrameRenderer::~GpuFrameRenderer() { destroy(); }
auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h) -> bool {
logical_w_ = logical_w;
logical_h_ = logical_h;
if (!device_.init(window)) {
return false;
}
if (!pipeline_.init(device_)) {
device_.destroy();
return false;
}
return true;
}
void GpuFrameRenderer::destroy() {
pipeline_.destroy();
device_.destroy();
vertices_.clear();
indices_.clear();
}
auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) -> bool {
SDL_GPUDevice* dev = device_.get();
if (dev == nullptr) {
return false;
}
cmd_buffer_ = SDL_AcquireGPUCommandBuffer(dev);
if (cmd_buffer_ == nullptr) {
std::cerr << "[GpuFrameRenderer] SDL_AcquireGPUCommandBuffer: " << SDL_GetError() << '\n';
return false;
}
if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(),
&swapchain_texture_, nullptr, nullptr)) {
std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n';
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
cmd_buffer_ = nullptr;
return false;
}
if (swapchain_texture_ == nullptr) {
// Ventana minimizada o swapchain no disponible: solo submit y salir.
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
cmd_buffer_ = nullptr;
return false;
}
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.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';
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
cmd_buffer_ = nullptr;
return false;
}
vertices_.clear();
indices_.clear();
return true;
}
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
}
// 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
// 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);
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();
// 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));
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);
// 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;
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: subir transfer buffer → device buffers.
// Importante: el copy pass debe ejecutarse 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 (load_op = LOAD para preservar lo ya pintado).
SDL_GPUColorTargetInfo color_target{};
color_target.texture = swapchain_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);
// Bind pipeline + buffers + uniforms.
SDL_BindGPUGraphicsPipeline(render_pass_, pipeline_.get());
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, // num_instances
0, // first_index
0, // vertex_offset
0); // first_instance
// 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::endFrame() {
if (cmd_buffer_ == nullptr) {
return;
}
flushBatch();
if (render_pass_ != nullptr) {
SDL_EndGPURenderPass(render_pass_);
render_pass_ = nullptr;
}
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
cmd_buffer_ = nullptr;
swapchain_texture_ = nullptr;
}
} // namespace Rendering::GPU
@@ -0,0 +1,76 @@
// 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
//
// 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.
#pragma once
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <cstdint>
#include <vector>
#include "core/rendering/gpu/gpu_device.hpp"
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
namespace Rendering::GPU {
class GpuFrameRenderer {
public:
GpuFrameRenderer() = default;
~GpuFrameRenderer();
GpuFrameRenderer(const GpuFrameRenderer&) = delete;
auto operator=(const GpuFrameRenderer&) -> GpuFrameRenderer& = delete;
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.
[[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).
[[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.
void endFrame();
// Acceso a internals (necesario para SDLManager y futuros sistemas).
[[nodiscard]] auto device() -> GpuDevice& { return device_; }
[[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; }
private:
GpuDevice device_;
GpuLinePipeline pipeline_;
// Tamaño lógico del juego (para transformación NDC en el shader).
float logical_w_{1280.0F};
float logical_h_{720.0F};
// Batch del frame en curso.
std::vector<LineVertex> vertices_;
std::vector<uint16_t> indices_;
// Estado del frame en curso.
SDL_GPUCommandBuffer* cmd_buffer_{nullptr};
SDL_GPUTexture* swapchain_texture_{nullptr};
SDL_GPURenderPass* render_pass_{nullptr};
// Helpers internos.
void flushBatch();
};
} // namespace Rendering::GPU
@@ -0,0 +1,115 @@
// gpu_line_pipeline.cpp - Implementación del pipeline de líneas
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <iostream>
#include "core/rendering/gpu/gpu_device.hpp"
namespace Rendering::GPU {
GpuLinePipeline::~GpuLinePipeline() { destroy(); }
auto GpuLinePipeline::init(const GpuDevice& device) -> bool {
owner_ = device.get();
if (owner_ == nullptr) {
return false;
}
SDL_GPUShader* vert = device.loadShader("line.vert.spv",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_uniform_buffers=*/1);
SDL_GPUShader* frag = device.loadShader("line.frag.spv",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_uniform_buffers=*/0);
if ((vert == nullptr) || (frag == nullptr)) {
if (vert != nullptr) {
SDL_ReleaseGPUShader(owner_, vert);
}
if (frag != nullptr) {
SDL_ReleaseGPUShader(owner_, frag);
}
std::cerr << "[GpuLinePipeline] Error cargando shaders\n";
return false;
}
// Vertex layout: pos (vec2) + color (vec4) → 6 floats por vertex.
SDL_GPUVertexBufferDescription vbo_desc{};
vbo_desc.slot = 0;
vbo_desc.pitch = sizeof(LineVertex);
vbo_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
vbo_desc.instance_step_rate = 0;
SDL_GPUVertexAttribute attrs[2];
attrs[0].location = 0; // in_position
attrs[0].buffer_slot = 0;
attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
attrs[0].offset = 0;
attrs[1].location = 1; // in_color
attrs[1].buffer_slot = 0;
attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
attrs[1].offset = sizeof(float) * 2;
SDL_GPUVertexInputState vertex_input{};
vertex_input.vertex_buffer_descriptions = &vbo_desc;
vertex_input.num_vertex_buffers = 1;
vertex_input.vertex_attributes = attrs;
vertex_input.num_vertex_attributes = 2;
// Color target = swapchain. Blending alpha estándar.
SDL_GPUColorTargetDescription color_target{};
color_target.format = device.swapchainFormat();
color_target.blend_state.enable_blend = true;
color_target.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
color_target.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
color_target.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD;
color_target.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
color_target.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
color_target.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
color_target.blend_state.color_write_mask =
SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G |
SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A;
SDL_GPUGraphicsPipelineTargetInfo target_info{};
target_info.color_target_descriptions = &color_target;
target_info.num_color_targets = 1;
target_info.has_depth_stencil_target = false;
SDL_GPUGraphicsPipelineCreateInfo info{};
info.vertex_shader = vert;
info.fragment_shader = frag;
info.vertex_input_state = vertex_input;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; // No backface culling para 2D
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
info.depth_stencil_state = {};
info.target_info = target_info;
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
// Los shaders se pueden liberar tras crear el pipeline (SDL los retiene
// internamente mientras el pipeline esté vivo).
SDL_ReleaseGPUShader(owner_, vert);
SDL_ReleaseGPUShader(owner_, frag);
if (pipeline_ == nullptr) {
std::cerr << "[GpuLinePipeline] SDL_CreateGPUGraphicsPipeline: " << SDL_GetError() << '\n';
return false;
}
return true;
}
void GpuLinePipeline::destroy() {
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
}
pipeline_ = nullptr;
owner_ = nullptr;
}
} // namespace Rendering::GPU
@@ -0,0 +1,56 @@
// gpu_line_pipeline.hpp - Pipeline gráfico para dibujar líneas vectoriales
// © 2025 Orni Attack
//
// Las líneas se renderizan como quads (2 triángulos = 6 índices) ya extrudidos
// en CPU según el grosor pedido por línea. Vertex layout: position (vec2) + color (vec4).
// Primitive type: TRIANGLELIST. Sin depth test (juego 2D), blending alpha estándar.
#pragma once
#include <SDL3/SDL_gpu.h>
#include <cstdint>
namespace Rendering::GPU {
class GpuDevice;
// Vertex layout (debe coincidir con shaders/line.vert.glsl).
struct LineVertex {
float x;
float y;
float r;
float g;
float b;
float a;
};
// Uniform buffer del vertex shader (debe coincidir con UBO en line.vert.glsl).
struct LineUniforms {
float viewport_width;
float viewport_height;
float padding_0;
float padding_1; // Alineamiento a 16 bytes
};
class GpuLinePipeline {
public:
GpuLinePipeline() = default;
~GpuLinePipeline();
GpuLinePipeline(const GpuLinePipeline&) = delete;
auto operator=(const GpuLinePipeline&) -> GpuLinePipeline& = delete;
GpuLinePipeline(GpuLinePipeline&&) = delete;
auto operator=(GpuLinePipeline&&) -> GpuLinePipeline& = delete;
[[nodiscard]] auto init(const GpuDevice& device) -> bool;
void destroy();
[[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; }
private:
SDL_GPUDevice* owner_{nullptr}; // No-owning; el GpuDevice es el dueño
SDL_GPUGraphicsPipeline* pipeline_{nullptr};
};
} // namespace Rendering::GPU