feat(gpu): suport Metal/MSL a macOS i shaders SPIR-V embedits

This commit is contained in:
2026-05-20 23:07:49 +02:00
parent ac5434fc30
commit 6259f594c8
15 changed files with 6823 additions and 227 deletions
+37 -34
View File
@@ -133,44 +133,47 @@ 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
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V (headers C++ embedits) ---
# Compila els shaders .glsl a SPIR-V i els converteix en headers C++ embedits
# (source/core/rendering/gpu/spv/*.h). Aquests headers es commiteen al repo:
# en macOS no cal glslc (els headers ja existeixen). En Linux/Windows glslc
# és obligatori per regenerar els headers en cada canvi del GLSL.
#
# Per a macOS hi ha a més els headers MSL escrits a mà a source/core/rendering/gpu/msl/.
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/shaders")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/gpu/spv")
set(ALL_SHADER_HEADERS
"${HEADERS_DIR}/line_vert_spv.h"
"${HEADERS_DIR}/line_frag_spv.h"
"${HEADERS_DIR}/postfx_vert_spv.h"
"${HEADERS_DIR}/postfx_frag_spv.h"
)
set(ALL_SHADER_SOURCES
"${SHADERS_DIR}/line.vert.glsl"
"${SHADERS_DIR}/line.frag.glsl"
"${SHADERS_DIR}/postfx.vert.glsl"
"${SHADERS_DIR}/postfx.frag.glsl"
)
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_custom_command(
OUTPUT ${ALL_SHADER_HEADERS}
COMMAND ${CMAKE_COMMAND}
-D GLSLC=${GLSLC_EXE}
-D SHADERS_DIR=${SHADERS_DIR}
-D HEADERS_DIR=${HEADERS_DIR}
-P ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
DEPENDS ${ALL_SHADER_SOURCES} ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
COMMENT "Compilant shaders GLSL → headers SPIR-V embedits"
VERBATIM
)
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
add_dependencies(${PROJECT_NAME} shaders)
message(STATUS "Shaders trobats: ${SHADER_SOURCES}")
message(STATUS "Shaders: glslc trobat (${GLSLC_EXE}); headers SPV es regeneraran si canvia el GLSL")
elseif(APPLE)
message(STATUS "Shaders: glslc no trobat en macOS — s'usaran els headers SPV ja commiteats")
else()
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V")
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V (obligatori a Linux/Windows)")
endif()
# --- STATIC ANALYSIS / FORMAT TARGETS ---
+38 -85
View File
@@ -1,105 +1,58 @@
// gpu_device.cpp - Implementación del wrapper de SDL_GPUDevice
// gpu_device.cpp - Implementació 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(); }
GpuDevice::~GpuDevice() { destroy(); }
auto GpuDevice::init(SDL_Window* window) -> bool {
window_ = window;
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;
// Selecció del format de shader per plataforma:
// - macOS → MSL (Metal Shading Language), shaders embedits a gpu/msl/*.msl.h
// - Resta → SPIR-V (Vulkan), shaders embedits a gpu/spv/*.h
#ifdef __APPLE__
constexpr SDL_GPUShaderFormat SHADER_FORMATS = SDL_GPU_SHADERFORMAT_MSL;
#else
constexpr SDL_GPUShaderFormat SHADER_FORMATS = SDL_GPU_SHADERFORMAT_SPIRV;
#endif
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_);
device_ = SDL_CreateGPUDevice(SHADER_FORMATS, /*debug_mode=*/true, /*name=*/nullptr);
if (device_ == nullptr) {
std::cerr << "[GpuDevice] SDL_CreateGPUDevice falló: " << SDL_GetError() << '\n';
return false;
}
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,
uint32_t num_samplers) const -> SDL_GPUShader* {
if (device_ == nullptr) {
return nullptr;
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;
}
// 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;
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;
}
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 = num_samplers;
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
+16 -24
View File
@@ -1,13 +1,17 @@
// gpu_device.hpp - Wrapper de SDL_GPUDevice
// © 2026 JailDesigner
//
// Ownership del SDL_GPUDevice y del claim del window. Backend preferido:
// Vulkan (Linux, Windows) y Metal (macOS). Sin DirectX.
// Ownership del SDL_GPUDevice i del claim del window. Backend per plataforma:
// Vulkan (Linux, Windows) i Metal (macOS). Sense DirectX.
//
// Uso:
// Els shaders s'embedien al binari (no es carreguen de disc): SPIR-V via
// headers generats per CMake a `gpu/spv/*.h`, i MSL a mà a `gpu/msl/*.msl.h`.
// La creació dels SDL_GPUShader la fan els pipelines via shader_factory.hpp.
//
// Ús:
// GpuDevice device;
// if (!device.init(window)) return -1; // claim del window
// ... renderer setup ...
// ... pipelines setup ...
// device.destroy(); // unclaim + destroy device
#pragma once
@@ -15,45 +19,33 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <string>
namespace Rendering::GPU {
class GpuDevice {
public:
class GpuDevice {
public:
GpuDevice() = default;
~GpuDevice();
// No copia / move (RAII propietario del device).
// No copia / move (RAII propietari 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.
// Crea el device i claim el window. Selecciona MSL en macOS, SPIR-V
// en la resta. Retorna false si no hi ha backend suportat o si el
// driver no permet 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).
// num_samplers: nº de samplers (combined image+sampler) usados por el shader.
[[nodiscard]] auto loadShader(const std::string& spv_filename,
SDL_GPUShaderStage stage,
uint32_t num_uniform_buffers,
uint32_t num_samplers = 0) 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:
private:
SDL_GPUDevice* device_{nullptr};
SDL_Window* window_{nullptr};
SDL_GPUTextureFormat swapchain_format_{SDL_GPU_TEXTUREFORMAT_INVALID};
};
};
} // namespace Rendering::GPU
@@ -1,4 +1,4 @@
// gpu_line_pipeline.cpp - Implementación del pipeline de líneas
// gpu_line_pipeline.cpp - Implementació del pipeline de línies
#include "core/rendering/gpu/gpu_line_pipeline.hpp"
@@ -8,6 +8,15 @@
#include <iostream>
#include "core/rendering/gpu/gpu_device.hpp"
#include "core/rendering/gpu/shader_factory.hpp"
#ifdef __APPLE__
#include "core/rendering/gpu/msl/line_frag.msl.h"
#include "core/rendering/gpu/msl/line_vert.msl.h"
#else
#include "core/rendering/gpu/spv/line_frag_spv.h"
#include "core/rendering/gpu/spv/line_vert_spv.h"
#endif
namespace Rendering::GPU {
@@ -20,12 +29,37 @@ namespace Rendering::GPU {
return false;
}
SDL_GPUShader* vert = device.loadShader("line.vert.spv",
// Vertex: 1 UBO (viewport_size). Fragment: cap recurs.
#ifdef __APPLE__
SDL_GPUShader* vert = createShaderMSL(owner_,
Msl::LINE_VERT_MSL,
"line_vs",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_samplers=*/0,
/*num_uniform_buffers=*/1);
SDL_GPUShader* frag = device.loadShader("line.frag.spv",
SDL_GPUShader* frag = createShaderMSL(owner_,
Msl::LINE_FRAG_MSL,
"line_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/0,
/*num_uniform_buffers=*/0);
#else
SDL_GPUShader* vert = createShaderSPIRV(owner_,
LINE_VERT_SPV,
LINE_VERT_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_samplers=*/0,
/*num_uniform_buffers=*/1);
SDL_GPUShader* frag = createShaderSPIRV(owner_,
LINE_FRAG_SPV,
LINE_FRAG_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/0,
/*num_uniform_buffers=*/0);
#endif
if ((vert == nullptr) || (frag == nullptr)) {
if (vert != nullptr) {
SDL_ReleaseGPUShader(owner_, vert);
@@ -33,7 +67,7 @@ namespace Rendering::GPU {
if (frag != nullptr) {
SDL_ReleaseGPUShader(owner_, frag);
}
std::cerr << "[GpuLinePipeline] Error cargando shaders\n";
std::cerr << "[GpuLinePipeline] Error cargando shaders: " << SDL_GetError() << '\n';
return false;
}
@@ -98,8 +132,8 @@ namespace Rendering::GPU {
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
// Los shaders se pueden liberar tras crear el pipeline (SDL los retiene
// internamente mientras el pipeline esté vivo).
// Els shaders es poden alliberar després de crear el pipeline (SDL els
// reté internament mentre el pipeline estigui viu).
SDL_ReleaseGPUShader(owner_, vert);
SDL_ReleaseGPUShader(owner_, frag);
+109 -78
View File
@@ -1,4 +1,4 @@
// gpu_postfx_pipeline.cpp - Implementación del pipeline de postprocesado.
// gpu_postfx_pipeline.cpp - Implementació del pipeline de postprocesat.
#include "core/rendering/gpu/gpu_postfx_pipeline.hpp"
@@ -8,91 +8,122 @@
#include <iostream>
#include "core/rendering/gpu/gpu_device.hpp"
#include "core/rendering/gpu/shader_factory.hpp"
#ifdef __APPLE__
#include "core/rendering/gpu/msl/postfx_frag.msl.h"
#include "core/rendering/gpu/msl/postfx_vert.msl.h"
#else
#include "core/rendering/gpu/spv/postfx_frag_spv.h"
#include "core/rendering/gpu/spv/postfx_vert_spv.h"
#endif
namespace Rendering::GPU {
GpuPostFxPipeline::~GpuPostFxPipeline() { destroy(); }
GpuPostFxPipeline::~GpuPostFxPipeline() { destroy(); }
auto GpuPostFxPipeline::init(const GpuDevice& device,
SDL_GPUTextureFormat target_format) -> bool {
owner_ = device.get();
if (owner_ == nullptr) {
return false;
}
// El vertex shader no usa UBO (emite tres vértices hardcodeados).
// El fragment shader usa 1 sampler (escena) y 1 UBO (parámetros postpro).
SDL_GPUShader* vert = device.loadShader("postfx.vert.spv",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_uniform_buffers=*/0,
/*num_samplers=*/0);
SDL_GPUShader* frag = device.loadShader("postfx.frag.spv",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_uniform_buffers=*/1,
/*num_samplers=*/1);
if ((vert == nullptr) || (frag == nullptr)) {
if (vert != nullptr) {
SDL_ReleaseGPUShader(owner_, vert);
auto GpuPostFxPipeline::init(const GpuDevice& device,
SDL_GPUTextureFormat target_format) -> bool {
owner_ = device.get();
if (owner_ == nullptr) {
return false;
}
if (frag != nullptr) {
SDL_ReleaseGPUShader(owner_, frag);
// Vertex shader: sense UBO ni vertex buffer (emet 3 vèrtexs hardcodejats).
// Fragment shader: 1 sampler (escena) + 1 UBO (paràmetres postpro).
#ifdef __APPLE__
SDL_GPUShader* vert = createShaderMSL(owner_,
Msl::POSTFX_VERT_MSL,
"postfx_vs",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_samplers=*/0,
/*num_uniform_buffers=*/0);
SDL_GPUShader* frag = createShaderMSL(owner_,
Msl::POSTFX_FRAG_MSL,
"postfx_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/1,
/*num_uniform_buffers=*/1);
#else
SDL_GPUShader* vert = createShaderSPIRV(owner_,
POSTFX_VERT_SPV,
POSTFX_VERT_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_VERTEX,
/*num_samplers=*/0,
/*num_uniform_buffers=*/0);
SDL_GPUShader* frag = createShaderSPIRV(owner_,
POSTFX_FRAG_SPV,
POSTFX_FRAG_SPV_SIZE,
"main",
SDL_GPU_SHADERSTAGE_FRAGMENT,
/*num_samplers=*/1,
/*num_uniform_buffers=*/1);
#endif
if ((vert == nullptr) || (frag == nullptr)) {
if (vert != nullptr) {
SDL_ReleaseGPUShader(owner_, vert);
}
if (frag != nullptr) {
SDL_ReleaseGPUShader(owner_, frag);
}
std::cerr << "[GpuPostFxPipeline] Error cargando shaders postfx: " << SDL_GetError() << '\n';
return false;
}
std::cerr << "[GpuPostFxPipeline] Error cargando shaders postfx\n";
return false;
// Sense vertex input: els tres vèrtexs del triangle es generen al shader.
SDL_GPUVertexInputState vertex_input{};
vertex_input.vertex_buffer_descriptions = nullptr;
vertex_input.num_vertex_buffers = 0;
vertex_input.vertex_attributes = nullptr;
vertex_input.num_vertex_attributes = 0;
// Color target del postpro = swapchain. Sense blending: el postpro reescriu
// píxels directament (la mescla amb l'escena ja es fa dins del shader).
SDL_GPUColorTargetDescription color_target{};
color_target.format = target_format;
color_target.blend_state.enable_blend = false;
color_target.blend_state.color_write_mask =
SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G |
SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A;
SDL_GPUGraphicsPipelineTargetInfo target_info{};
target_info.color_target_descriptions = &color_target;
target_info.num_color_targets = 1;
target_info.has_depth_stencil_target = false;
SDL_GPUGraphicsPipelineCreateInfo info{};
info.vertex_shader = vert;
info.fragment_shader = frag;
info.vertex_input_state = vertex_input;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
info.depth_stencil_state = {};
info.target_info = target_info;
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
SDL_ReleaseGPUShader(owner_, vert);
SDL_ReleaseGPUShader(owner_, frag);
if (pipeline_ == nullptr) {
std::cerr << "[GpuPostFxPipeline] SDL_CreateGPUGraphicsPipeline: "
<< SDL_GetError() << '\n';
return false;
}
return true;
}
// Sin vertex input: los tres vértices del triángulo se generan en el shader.
SDL_GPUVertexInputState vertex_input{};
vertex_input.vertex_buffer_descriptions = nullptr;
vertex_input.num_vertex_buffers = 0;
vertex_input.vertex_attributes = nullptr;
vertex_input.num_vertex_attributes = 0;
// Color target del postpro = swapchain. Sin blending: el postpro reescribe
// píxeles directamente (la mezcla con la escena ya se hizo dentro del shader).
SDL_GPUColorTargetDescription color_target{};
color_target.format = target_format;
color_target.blend_state.enable_blend = false;
color_target.blend_state.color_write_mask =
SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G |
SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A;
SDL_GPUGraphicsPipelineTargetInfo target_info{};
target_info.color_target_descriptions = &color_target;
target_info.num_color_targets = 1;
target_info.has_depth_stencil_target = false;
SDL_GPUGraphicsPipelineCreateInfo info{};
info.vertex_shader = vert;
info.fragment_shader = frag;
info.vertex_input_state = vertex_input;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
info.depth_stencil_state = {};
info.target_info = target_info;
pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info);
SDL_ReleaseGPUShader(owner_, vert);
SDL_ReleaseGPUShader(owner_, frag);
if (pipeline_ == nullptr) {
std::cerr << "[GpuPostFxPipeline] SDL_CreateGPUGraphicsPipeline: "
<< SDL_GetError() << '\n';
return false;
void GpuPostFxPipeline::destroy() {
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
}
pipeline_ = nullptr;
owner_ = nullptr;
}
return true;
}
void GpuPostFxPipeline::destroy() {
if ((pipeline_ != nullptr) && (owner_ != nullptr)) {
SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_);
}
pipeline_ = nullptr;
owner_ = nullptr;
}
} // namespace Rendering::GPU
@@ -0,0 +1,34 @@
// line_frag.msl.h - Metal Shading Language del fragment shader de línies
// © 2026 JailDesigner
//
// IMPORTANT: mantenir sincronitzat a mà amb shaders/line.frag.glsl. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic.
//
// Aplica antialias geomètric via smoothstep sobre edge_dist (±1 als laterals,
// 0 al centre del quad). Sense uniforms ni samplers.
#pragma once
#ifdef __APPLE__
namespace Rendering::GPU::Msl {
inline constexpr const char* LINE_FRAG_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct VOut {
float4 pos [[position]];
float4 color;
float edge_dist;
};
fragment float4 line_fs(VOut in [[stage_in]]) {
float d = abs(in.edge_dist);
float alpha = 1.0 - smoothstep(0.7, 1.0, d);
return float4(in.color.rgb, in.color.a * alpha);
}
)";
} // namespace Rendering::GPU::Msl
#endif // __APPLE__
@@ -0,0 +1,54 @@
// line_vert.msl.h - Metal Shading Language del vertex shader de línies
// © 2026 JailDesigner
//
// IMPORTANT: mantenir sincronitzat a mà amb shaders/line.vert.glsl. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
// canvi al UBO o al layout de vèrtex cal replicar-lo ací al mateix commit.
//
// Mapping SDL3 GPU → Metal (segons src/gpu/metal/SDL_gpu_metal.m upstream):
// - Vertex buffers d'usuari: [[buffer(14 + slot)]] (METAL_FIRST_VERTEX_BUFFER_SLOT=14).
// A través de [[stage_in]] amb [[attribute(N)]], Metal resol automàticament
// a partir del MTLVertexDescriptor del pipeline state (transparent al MSL).
// - Vertex UBO slot 0 SDL → [[buffer(0)]] MSL (sense offset).
#pragma once
#ifdef __APPLE__
namespace Rendering::GPU::Msl {
inline constexpr const char* LINE_VERT_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct VIn {
float2 in_position [[attribute(0)]];
float4 in_color [[attribute(1)]];
float in_edge_dist[[attribute(2)]];
};
struct VOut {
float4 pos [[position]];
float4 color;
float edge_dist;
};
struct LineUBO {
float2 viewport_size;
float2 _padding;
};
vertex VOut line_vs(VIn in [[stage_in]],
constant LineUBO& ubo [[buffer(0)]]) {
float2 ndc = (in.in_position / ubo.viewport_size) * 2.0 - 1.0;
ndc.y = -ndc.y;
VOut out;
out.pos = float4(ndc, 0.0, 1.0);
out.color = in.in_color;
out.edge_dist = in.in_edge_dist;
return out;
}
)";
} // namespace Rendering::GPU::Msl
#endif // __APPLE__
@@ -0,0 +1,90 @@
// postfx_frag.msl.h - Metal Shading Language del fragment shader del postpro
// © 2026 JailDesigner
//
// IMPORTANT: mantenir sincronitzat a mà amb shaders/postfx.frag.glsl. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
// canvi al struct PostFxUniforms (gpu_postfx_pipeline.hpp), al GLSL o al MSL
// cal replicar-lo a totes tres al mateix commit.
//
// Composició final: bloom 5×5 amb high-pass, flicker sinusoidal global,
// background pulse sumat. Recursos:
// - texture2d<float> scene [[texture(0)]] + sampler [[sampler(0)]]
// - constant PostFxUBO& ubo [[buffer(0)]] (slot 0 SDL → buffer(0) MSL)
//
// L'struct PostFxUBO té layout idèntic a PostFxUniforms (5×vec4 = 80 bytes).
#pragma once
#ifdef __APPLE__
namespace Rendering::GPU::Msl {
inline constexpr const char* POSTFX_FRAG_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct PostFxUBO {
float time;
float bloom_intensity;
float bloom_threshold;
float bloom_radius_px;
float flicker_amplitude;
float flicker_frequency_hz;
float background_pulse_freq_hz;
float pad_a;
float4 background_min;
float4 background_max;
float2 texel_size;
float2 pad_b;
};
constant float TAU = 6.28318530718;
fragment float4 postfx_fs(PostVOut in [[stage_in]],
texture2d<float> scene [[texture(0)]],
sampler samp [[sampler(0)]],
constant PostFxUBO& ubo [[buffer(0)]]) {
// === BLOOM ===
float3 src = scene.sample(samp, in.uv).rgb;
float3 bloom = float3(0.0);
float total_weight = 0.0;
for (int dy = -2; dy <= 2; ++dy) {
for (int dx = -2; dx <= 2; ++dx) {
float2 offset = float2(float(dx), float(dy)) * ubo.texel_size * ubo.bloom_radius_px;
float3 c = scene.sample(samp, in.uv + offset).rgb;
float luma = max(c.r, max(c.g, c.b));
float high_pass = max(0.0, luma - ubo.bloom_threshold);
float w = exp(-float(dx * dx + dy * dy) / 4.0);
bloom += c * high_pass * w;
total_weight += w;
}
}
if (total_weight > 0.0) {
bloom /= total_weight;
}
bloom *= ubo.bloom_intensity;
// === FLICKER ===
float pulse = (sin(ubo.time * ubo.flicker_frequency_hz * TAU) * 0.5) + 0.5;
float flicker = 1.0 - (ubo.flicker_amplitude * (1.0 - pulse));
// === BACKGROUND PULSE ===
float bg_pulse = (sin(ubo.time * ubo.background_pulse_freq_hz * TAU) * 0.5) + 0.5;
float3 background = mix(ubo.background_min.rgb, ubo.background_max.rgb, bg_pulse);
// === COMPOSICIÓ ===
float3 lines_and_glow = (src + bloom) * flicker;
return float4(background + lines_and_glow, 1.0);
}
)";
} // namespace Rendering::GPU::Msl
#endif // __APPLE__
@@ -0,0 +1,38 @@
// postfx_vert.msl.h - Metal Shading Language del vertex shader del postpro
// © 2026 JailDesigner
//
// IMPORTANT: mantenir sincronitzat a mà amb shaders/postfx.vert.glsl. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic.
//
// Fullscreen triangle: 3 vèrtexs en (-1,-1), (3,-1), (-1,3) generats per
// [[vertex_id]]. UV.y invertida per compensar la diferència de convenció
// entre clip-space del line shader (Y-flip) i el mostreig SDL_gpu (origen
// top-left). Sense uniforms ni vertex buffers (DrawPrimitives vertex_count=3).
#pragma once
#ifdef __APPLE__
namespace Rendering::GPU::Msl {
inline constexpr const char* POSTFX_VERT_MSL = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
const float2 positions[3] = { {-1.0, -1.0}, { 3.0, -1.0}, {-1.0, 3.0} };
const float2 uvs[3] = { { 0.0, 1.0}, { 2.0, 1.0}, { 0.0, -1.0} };
PostVOut out;
out.pos = float4(positions[vid], 0.0, 1.0);
out.uv = uvs[vid];
return out;
}
)";
} // namespace Rendering::GPU::Msl
#endif // __APPLE__
@@ -0,0 +1,60 @@
// shader_factory.hpp - Helpers per crear SDL_GPUShader segons plataforma
// © 2026 JailDesigner
//
// En __APPLE__ s'utilitza MSL (Metal Shading Language) embedit com a string
// literal C++. En la resta de plataformes (Linux/Windows) s'utilitza SPIR-V
// embedit com a uint8_t[] en headers generats per CMake. La selecció és
// compile-time via #ifdef.
#pragma once
#include <SDL3/SDL_gpu.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace Rendering::GPU {
#ifdef __APPLE__
inline auto createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info{};
info.code = reinterpret_cast<const Uint8*>(msl_source);
info.code_size = std::strlen(msl_source) + 1;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_MSL;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_uniform_buffers = num_uniform_buffers;
info.num_storage_buffers = 0;
info.num_storage_textures = 0;
return SDL_CreateGPUShader(device, &info);
}
#else
inline auto createShaderSPIRV(SDL_GPUDevice* device,
const std::uint8_t* spv_code,
std::size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info{};
info.code = spv_code;
info.code_size = spv_size;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_uniform_buffers = num_uniform_buffers;
info.num_storage_buffers = 0;
info.num_storage_textures = 0;
return SDL_CreateGPUShader(device, &info);
}
#endif
} // namespace Rendering::GPU
@@ -0,0 +1,670 @@
#pragma once
#include <cstddef>
#include <cstdint>
static const uint8_t LINE_FRAG_SPV[] = {
0x03,
0x02,
0x23,
0x07,
0x00,
0x00,
0x01,
0x00,
0x0b,
0x00,
0x0d,
0x00,
0x25,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x11,
0x00,
0x02,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x06,
0x00,
0x01,
0x00,
0x00,
0x00,
0x47,
0x4c,
0x53,
0x4c,
0x2e,
0x73,
0x74,
0x64,
0x2e,
0x34,
0x35,
0x30,
0x00,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x03,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x08,
0x00,
0x04,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x6d,
0x61,
0x69,
0x6e,
0x00,
0x00,
0x00,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x15,
0x00,
0x00,
0x00,
0x17,
0x00,
0x00,
0x00,
0x10,
0x00,
0x03,
0x00,
0x04,
0x00,
0x00,
0x00,
0x07,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x15,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x17,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x13,
0x00,
0x02,
0x00,
0x02,
0x00,
0x00,
0x00,
0x21,
0x00,
0x03,
0x00,
0x03,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x16,
0x00,
0x03,
0x00,
0x06,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x09,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x09,
0x00,
0x00,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x2b,
0x00,
0x04,
0x00,
0x06,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x80,
0x3f,
0x2b,
0x00,
0x04,
0x00,
0x06,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x33,
0x33,
0x33,
0x3f,
0x17,
0x00,
0x04,
0x00,
0x13,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x14,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x14,
0x00,
0x00,
0x00,
0x15,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x16,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x16,
0x00,
0x00,
0x00,
0x17,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x15,
0x00,
0x04,
0x00,
0x1b,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x2b,
0x00,
0x04,
0x00,
0x1b,
0x00,
0x00,
0x00,
0x1c,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x36,
0x00,
0x05,
0x00,
0x02,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0xf8,
0x00,
0x02,
0x00,
0x05,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x06,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x0c,
0x00,
0x06,
0x00,
0x06,
0x00,
0x00,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0c,
0x00,
0x08,
0x00,
0x06,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x31,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x83,
0x00,
0x05,
0x00,
0x06,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x13,
0x00,
0x00,
0x00,
0x19,
0x00,
0x00,
0x00,
0x17,
0x00,
0x00,
0x00,
0x41,
0x00,
0x05,
0x00,
0x09,
0x00,
0x00,
0x00,
0x1d,
0x00,
0x00,
0x00,
0x17,
0x00,
0x00,
0x00,
0x1c,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x06,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x1d,
0x00,
0x00,
0x00,
0x85,
0x00,
0x05,
0x00,
0x06,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x51,
0x00,
0x05,
0x00,
0x06,
0x00,
0x00,
0x00,
0x21,
0x00,
0x00,
0x00,
0x19,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x51,
0x00,
0x05,
0x00,
0x06,
0x00,
0x00,
0x00,
0x22,
0x00,
0x00,
0x00,
0x19,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x51,
0x00,
0x05,
0x00,
0x06,
0x00,
0x00,
0x00,
0x23,
0x00,
0x00,
0x00,
0x19,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x50,
0x00,
0x07,
0x00,
0x13,
0x00,
0x00,
0x00,
0x24,
0x00,
0x00,
0x00,
0x21,
0x00,
0x00,
0x00,
0x22,
0x00,
0x00,
0x00,
0x23,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x3e,
0x00,
0x03,
0x00,
0x15,
0x00,
0x00,
0x00,
0x24,
0x00,
0x00,
0x00,
0xfd,
0x00,
0x01,
0x00,
0x38,
0x00,
0x01,
0x00,
};
static const size_t LINE_FRAG_SPV_SIZE = 664;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+99
View File
@@ -0,0 +1,99 @@
# compile_spirv.cmake
# Compila shaders GLSL a SPIR-V i genera headers C++ embedibles.
# Multiplataforma: Windows, macOS, Linux (no requereix bash ni xxd).
#
# Invocat per CMakeLists.txt amb:
# cmake -D GLSLC=<path> -D SHADERS_DIR=<path> -D HEADERS_DIR=<path> -P compile_spirv.cmake
#
# També es pot executar manualment des de l'arrel del projecte:
# cmake -D GLSLC=glslc -D SHADERS_DIR=shaders \
# -D HEADERS_DIR=source/core/rendering/gpu/spv \
# -P tools/shaders/compile_spirv.cmake
cmake_minimum_required(VERSION 3.10)
cmake_policy(SET CMP0007 NEW)
# Llista de shaders a compilar: font relativa a SHADERS_DIR
set(SHADER_SOURCES
"line.vert.glsl"
"line.frag.glsl"
"postfx.vert.glsl"
"postfx.frag.glsl"
)
# Nom de la variable C++ per a cada shader (mateix ordre).
# UPPER_CASE perquè són constexpr globals (.clang-tidy ho exigeix).
set(SHADER_VARS
"LINE_VERT_SPV"
"LINE_FRAG_SPV"
"POSTFX_VERT_SPV"
"POSTFX_FRAG_SPV"
)
# Flags extra per a cada shader (necessaris perquè .vert.glsl/.frag.glsl no s'infereixen)
set(SHADER_FLAGS
"-fshader-stage=vert"
"-fshader-stage=frag"
"-fshader-stage=vert"
"-fshader-stage=frag"
)
list(LENGTH SHADER_SOURCES NUM_SHADERS)
math(EXPR LAST_IDX "${NUM_SHADERS} - 1")
foreach(IDX RANGE ${LAST_IDX})
list(GET SHADER_SOURCES ${IDX} SRC_NAME)
list(GET SHADER_VARS ${IDX} VAR)
list(GET SHADER_FLAGS ${IDX} EXTRA_FLAG)
# Derivem el nom del header a partir de la variable: LINE_VERT_SPV → line_vert_spv.h
string(TOLOWER "${VAR}" HDR_BASE)
set(SRC "${SHADERS_DIR}/${SRC_NAME}")
set(SPV "${HEADERS_DIR}/${HDR_BASE}.spv")
set(HDR "${HEADERS_DIR}/${HDR_BASE}.h")
message(STATUS "Compilant ${SRC} ...")
if(EXTRA_FLAG)
execute_process(
COMMAND "${GLSLC}" "${EXTRA_FLAG}" -O "${SRC}" -o "${SPV}"
RESULT_VARIABLE GLSLC_RESULT
ERROR_VARIABLE GLSLC_ERROR
)
else()
execute_process(
COMMAND "${GLSLC}" -O "${SRC}" -o "${SPV}"
RESULT_VARIABLE GLSLC_RESULT
ERROR_VARIABLE GLSLC_ERROR
)
endif()
if(NOT GLSLC_RESULT EQUAL 0)
message(FATAL_ERROR "glslc ha fallat per a ${SRC}:\n${GLSLC_ERROR}")
endif()
# Llegim el binari SPV com a hex (sense separadors) i el dividim en bytes.
file(READ "${SPV}" HEX_DATA HEX)
string(REGEX MATCHALL ".." BYTES "${HEX_DATA}")
list(LENGTH BYTES NUM_BYTES)
set(ARRAY_BODY "")
foreach(BYTE ${BYTES})
string(APPEND ARRAY_BODY " 0x${BYTE},\n")
endforeach()
file(WRITE "${HDR}"
"#pragma once\n"
"#include <cstddef>\n"
"#include <cstdint>\n"
"static const uint8_t ${VAR}[] = {\n"
"${ARRAY_BODY}"
"};\n"
"static const size_t ${VAR}_SIZE = ${NUM_BYTES};\n"
)
file(REMOVE "${SPV}")
message(STATUS " -> ${HDR} (${NUM_BYTES} bytes)")
endforeach()
message(STATUS "Shaders SPIR-V compilats correctament.")