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:
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user