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
+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