From ba6fd00b54c70b6a99f2d8d69d9ba246996743df Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 19 May 2026 14:01:34 +0200 Subject: [PATCH] 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) --- CMakeLists.txt | 40 ++++ MIGRATION_PLAN.md | 52 +++-- shaders/line.frag.glsl | 12 + shaders/line.vert.glsl | 29 +++ source/core/rendering/gpu/gpu_device.cpp | 104 +++++++++ source/core/rendering/gpu/gpu_device.hpp | 57 +++++ .../core/rendering/gpu/gpu_frame_renderer.cpp | 213 ++++++++++++++++++ .../core/rendering/gpu/gpu_frame_renderer.hpp | 76 +++++++ .../core/rendering/gpu/gpu_line_pipeline.cpp | 115 ++++++++++ .../core/rendering/gpu/gpu_line_pipeline.hpp | 56 +++++ 10 files changed, 733 insertions(+), 21 deletions(-) create mode 100644 shaders/line.frag.glsl create mode 100644 shaders/line.vert.glsl create mode 100644 source/core/rendering/gpu/gpu_device.cpp create mode 100644 source/core/rendering/gpu/gpu_device.hpp create mode 100644 source/core/rendering/gpu/gpu_frame_renderer.cpp create mode 100644 source/core/rendering/gpu/gpu_frame_renderer.hpp create mode 100644 source/core/rendering/gpu/gpu_line_pipeline.cpp create mode 100644 source/core/rendering/gpu/gpu_line_pipeline.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8777d7c..bdb14ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md index 7640a7a..e08efbd 100644 --- a/MIGRATION_PLAN.md +++ b/MIGRATION_PLAN.md @@ -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 diff --git a/shaders/line.frag.glsl b/shaders/line.frag.glsl new file mode 100644 index 0000000..086e216 --- /dev/null +++ b/shaders/line.frag.glsl @@ -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; +} diff --git a/shaders/line.vert.glsl b/shaders/line.vert.glsl new file mode 100644 index 0000000..247d3ff --- /dev/null +++ b/shaders/line.vert.glsl @@ -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; +} diff --git a/source/core/rendering/gpu/gpu_device.cpp b/source/core/rendering/gpu/gpu_device.cpp new file mode 100644 index 0000000..92b0952 --- /dev/null +++ b/source/core/rendering/gpu/gpu_device.cpp @@ -0,0 +1,104 @@ +// gpu_device.cpp - Implementación del wrapper de SDL_GPUDevice + +#include "core/rendering/gpu/gpu_device.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#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 buffer(static_cast(SIZE)); + if (!file.read(reinterpret_cast(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 diff --git a/source/core/rendering/gpu/gpu_device.hpp b/source/core/rendering/gpu/gpu_device.hpp new file mode 100644 index 0000000..8099bb7 --- /dev/null +++ b/source/core/rendering/gpu/gpu_device.hpp @@ -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 +#include + +#include + +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 diff --git a/source/core/rendering/gpu/gpu_frame_renderer.cpp b/source/core/rendering/gpu/gpu_frame_renderer.cpp new file mode 100644 index 0000000..4cdb5c4 --- /dev/null +++ b/source/core/rendering/gpu/gpu_frame_renderer.cpp @@ -0,0 +1,213 @@ +// gpu_frame_renderer.cpp - Implementación del FrameRenderer + +#include "core/rendering/gpu/gpu_frame_renderer.hpp" + +#include +#include + +#include +#include +#include +#include + +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(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(vertices_.size() * sizeof(LineVertex)); + const uint32_t IBO_SIZE = static_cast(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(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(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 diff --git a/source/core/rendering/gpu/gpu_frame_renderer.hpp b/source/core/rendering/gpu/gpu_frame_renderer.hpp new file mode 100644 index 0000000..9a5ddd6 --- /dev/null +++ b/source/core/rendering/gpu/gpu_frame_renderer.hpp @@ -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 +#include + +#include +#include + +#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 vertices_; + std::vector 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 diff --git a/source/core/rendering/gpu/gpu_line_pipeline.cpp b/source/core/rendering/gpu/gpu_line_pipeline.cpp new file mode 100644 index 0000000..4a34016 --- /dev/null +++ b/source/core/rendering/gpu/gpu_line_pipeline.cpp @@ -0,0 +1,115 @@ +// gpu_line_pipeline.cpp - Implementación del pipeline de líneas + +#include "core/rendering/gpu/gpu_line_pipeline.hpp" + +#include +#include + +#include + +#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 diff --git a/source/core/rendering/gpu/gpu_line_pipeline.hpp b/source/core/rendering/gpu/gpu_line_pipeline.hpp new file mode 100644 index 0000000..a11511d --- /dev/null +++ b/source/core/rendering/gpu/gpu_line_pipeline.hpp @@ -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 + +#include + +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