From d6ffbda00d52791c26982f1ae64b65d5ae6c4596 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 2 Oct 2025 21:05:28 +0200 Subject: [PATCH] WIP: Metal shader backend para macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shaders MSL portados desde GLSL (vertex + fragment) - Estructura básica de MetalShader class - Device, command queue, pipeline y buffers creados - CMakeLists.txt actualizado con Metal frameworks - assets.txt incluye shaders .metal como opcionales PENDIENTE: - Implementar render() loop completo - Obtener MTLTexture desde SDL_Texture - Crear sampler state - Testing en macOS real Ver METAL_BACKEND_NOTES.md para detalles de implementación. --- CMakeLists.txt | 13 + config/assets.txt | 6 +- data/shaders/crtpi_fragment.metal | 152 +++++++++ data/shaders/crtpi_vertex.metal | 46 +++ source/rendering/metal/METAL_BACKEND_NOTES.md | 166 ++++++++++ source/rendering/metal/metal_shader.h | 76 +++++ source/rendering/metal/metal_shader.mm | 301 ++++++++++++++++++ source/screen.cpp | 3 + 8 files changed, 762 insertions(+), 1 deletion(-) create mode 100644 data/shaders/crtpi_fragment.metal create mode 100644 data/shaders/crtpi_vertex.metal create mode 100644 source/rendering/metal/METAL_BACKEND_NOTES.md create mode 100644 source/rendering/metal/metal_shader.h create mode 100644 source/rendering/metal/metal_shader.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a17012..847ed43 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,14 @@ set(RENDERING_SOURCES source/rendering/opengl/opengl_shader.cpp ) +# Añadir backend de Metal en macOS +if(APPLE) + list(APPEND RENDERING_SOURCES + source/rendering/metal/metal_shader.mm + ) + message(STATUS "Metal backend habilitado para macOS") +endif() + # Configuración de SDL3 find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3) message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}") @@ -159,6 +167,11 @@ elseif(APPLE) target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUILD) target_compile_options(${PROJECT_NAME} PRIVATE -Wno-deprecated) set(CMAKE_OSX_ARCHITECTURES "arm64") + # Enlazar frameworks de Metal + target_link_libraries(${PROJECT_NAME} PRIVATE + "-framework Metal" + "-framework QuartzCore" + ) elseif(UNIX AND NOT APPLE) target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD) endif() diff --git a/config/assets.txt b/config/assets.txt index 59bd652..349e663 100644 --- a/config/assets.txt +++ b/config/assets.txt @@ -74,10 +74,14 @@ SOUND|${PREFIX}/data/sound/voice_recover.wav SOUND|${PREFIX}/data/sound/voice_thankyou.wav SOUND|${PREFIX}/data/sound/walk.wav -# Shaders +# Shaders OpenGL (Windows/Linux) DATA|${PREFIX}/data/shaders/crtpi_vertex.glsl DATA|${PREFIX}/data/shaders/crtpi_fragment.glsl +# Shaders Metal (macOS) - opcionales +DATA|${PREFIX}/data/shaders/crtpi_vertex.metal|optional +DATA|${PREFIX}/data/shaders/crtpi_fragment.metal|optional + # Texturas - Balloons ANIMATION|${PREFIX}/data/gfx/balloon/balloon0.ani ANIMATION|${PREFIX}/data/gfx/balloon/balloon1.ani diff --git a/data/shaders/crtpi_fragment.metal b/data/shaders/crtpi_fragment.metal new file mode 100644 index 0000000..101c117 --- /dev/null +++ b/data/shaders/crtpi_fragment.metal @@ -0,0 +1,152 @@ +// +// CRT-Pi Fragment Shader - Metal Shading Language +// Portado desde GLSL a MSL para macOS +// + +#include +using namespace metal; + +// Configuración (equivalente a los #define en GLSL) +constant bool SCANLINES = true; +constant bool MULTISAMPLE = true; +constant bool GAMMA = true; +constant bool FAKE_GAMMA = false; +constant bool CURVATURE = false; +constant bool SHARPER = false; +constant int MASK_TYPE = 2; // 0=none, 1=green/magenta, 2=trinitron + +constant float CURVATURE_X = 0.05; +constant float CURVATURE_Y = 0.1; +constant float MASK_BRIGHTNESS = 0.80; +constant float SCANLINE_WEIGHT = 6.0; +constant float SCANLINE_GAP_BRIGHTNESS = 0.12; +constant float BLOOM_FACTOR = 3.5; +constant float INPUT_GAMMA = 2.4; +constant float OUTPUT_GAMMA = 2.2; + +// Estructura de entrada (salida del vertex shader) +struct VertexOut { + float4 position [[position]]; + float2 texCoord; + float filterWidth; + // float2 screenScale; // Solo si CURVATURE está activo +}; + +// Uniforms +struct Uniforms { + float2 textureSize; +}; + +// Función para calcular el peso de la scanline +float CalcScanLineWeight(float dist) { + return max(1.0 - dist * dist * SCANLINE_WEIGHT, SCANLINE_GAP_BRIGHTNESS); +} + +// Función para calcular scanline con multisampling +float CalcScanLine(float dy, float filterWidth) { + float scanLineWeight = CalcScanLineWeight(dy); + if (MULTISAMPLE) { + scanLineWeight += CalcScanLineWeight(dy - filterWidth); + scanLineWeight += CalcScanLineWeight(dy + filterWidth); + scanLineWeight *= 0.3333333; + } + return scanLineWeight; +} + +// Entry point del fragment shader +fragment float4 fragment_main(VertexOut in [[stage_in]], + texture2d colorTexture [[texture(0)]], + constant Uniforms& uniforms [[buffer(1)]], + sampler textureSampler [[sampler(0)]]) { + + float2 texcoord = in.texCoord; + + // Si CURVATURE estuviera activo, aquí iría la función Distort() + + float2 texcoordInPixels = texcoord * uniforms.textureSize; + + float2 tc; + float scanLineWeight; + + if (!SHARPER) { + // Modo normal (no SHARPER) + float tempY = floor(texcoordInPixels.y) + 0.5; + float yCoord = tempY / uniforms.textureSize.y; + float dy = texcoordInPixels.y - tempY; + scanLineWeight = CalcScanLine(dy, in.filterWidth); + float signY = sign(dy); + dy = dy * dy; + dy = dy * dy; + dy *= 8.0; + dy /= uniforms.textureSize.y; + dy *= signY; + tc = float2(texcoord.x, yCoord + dy); + } else { + // Modo SHARPER + float2 tempCoord = floor(texcoordInPixels) + 0.5; + float2 coord = tempCoord / uniforms.textureSize; + float2 deltas = texcoordInPixels - tempCoord; + scanLineWeight = CalcScanLine(deltas.y, in.filterWidth); + float2 signs = sign(deltas); + deltas.x *= 2.0; + deltas = deltas * deltas; + deltas.y = deltas.y * deltas.y; + deltas.x *= 0.5; + deltas.y *= 8.0; + deltas /= uniforms.textureSize; + deltas *= signs; + tc = coord + deltas; + } + + // Muestrear textura (texture() en GLSL = sample() en MSL) + float3 colour = colorTexture.sample(textureSampler, tc).rgb; + + if (SCANLINES) { + if (GAMMA) { + if (FAKE_GAMMA) { + colour = colour * colour; + } else { + colour = pow(colour, float3(INPUT_GAMMA)); + } + } + scanLineWeight *= BLOOM_FACTOR; + colour *= scanLineWeight; + + if (GAMMA) { + if (FAKE_GAMMA) { + colour = sqrt(colour); + } else { + colour = pow(colour, float3(1.0 / OUTPUT_GAMMA)); + } + } + } + + // Aplicar máscara CRT + if (MASK_TYPE == 0) { + return float4(colour, 1.0); + } else if (MASK_TYPE == 1) { + // Máscara verde/magenta + float whichMask = fract(in.position.x * 0.5); + float3 mask; + if (whichMask < 0.5) { + mask = float3(MASK_BRIGHTNESS, 1.0, MASK_BRIGHTNESS); + } else { + mask = float3(1.0, MASK_BRIGHTNESS, 1.0); + } + return float4(colour * mask, 1.0); + } else if (MASK_TYPE == 2) { + // Máscara trinitron + float whichMask = fract(in.position.x * 0.3333333); + float3 mask = float3(MASK_BRIGHTNESS, MASK_BRIGHTNESS, MASK_BRIGHTNESS); + if (whichMask < 0.3333333) { + mask.x = 1.0; + } else if (whichMask < 0.6666666) { + mask.y = 1.0; + } else { + mask.z = 1.0; + } + return float4(colour * mask, 1.0); + } + + return float4(colour, 1.0); +} diff --git a/data/shaders/crtpi_vertex.metal b/data/shaders/crtpi_vertex.metal new file mode 100644 index 0000000..db35b2c --- /dev/null +++ b/data/shaders/crtpi_vertex.metal @@ -0,0 +1,46 @@ +// +// CRT-Pi Vertex Shader - Metal Shading Language +// Portado desde GLSL a MSL para macOS +// + +#include +using namespace metal; + +// Estructura de entrada del vertex shader (desde el buffer de vértices) +struct VertexIn { + float2 position [[attribute(0)]]; // Posición del vértice + float2 texCoord [[attribute(1)]]; // Coordenadas de textura +}; + +// Estructura de salida del vertex shader (entrada al fragment shader) +struct VertexOut { + float4 position [[position]]; // Posición en clip space + float2 texCoord; // Coordenadas de textura + float filterWidth; // Ancho del filtro calculado + // float2 screenScale; // Solo si CURVATURE está activo +}; + +// Uniforms (constantes del shader) +struct Uniforms { + float2 textureSize; // Tamaño de la textura (width, height) +}; + +// Entry point del vertex shader +vertex VertexOut vertex_main(VertexIn in [[stage_in]], + constant Uniforms& uniforms [[buffer(1)]]) { + VertexOut out; + + // Posición del vértice (ya está en espacio de clip [-1, 1]) + out.position = float4(in.position, 0.0, 1.0); + + // Pasar coordenadas de textura (invertir Y para SDL, igual que en GLSL) + out.texCoord = float2(in.texCoord.x, 1.0 - in.texCoord.y) * 1.0001; + + // Calcular filterWidth dinámicamente basándose en la altura de la textura + out.filterWidth = (768.0 / uniforms.textureSize.y) / 3.0; + + // Si CURVATURE estuviera activo: + // out.screenScale = float2(1.0, 1.0); + + return out; +} diff --git a/source/rendering/metal/METAL_BACKEND_NOTES.md b/source/rendering/metal/METAL_BACKEND_NOTES.md new file mode 100644 index 0000000..ac5626e --- /dev/null +++ b/source/rendering/metal/METAL_BACKEND_NOTES.md @@ -0,0 +1,166 @@ +# Metal Shader Backend - Notas de Implementación + +## Estado Actual + +✅ **Completado:** +- Shaders MSL (Metal Shading Language) portados desde GLSL +- Estructura básica de `MetalShader` class +- Inicialización de Metal device y command queue +- Compilación de shaders en runtime +- Creación de pipeline state +- Buffers de vértices, índices y uniforms + +❌ **Pendiente:** +- **Render loop completo** (la parte más crítica) +- Obtener textura Metal desde SDL_Texture +- Gestión de drawables y presentation + +## Diferencias GLSL vs MSL + +| Concepto | GLSL (OpenGL) | MSL (Metal) | +|----------|---------------|-------------| +| Entrada vertex | `layout(location = 0) in vec2` | `[[attribute(0)]]` | +| Salida vertex | `out vec2` | Struct con `[[position]]` | +| Uniforms | `uniform vec2` | `constant` struct en `[[buffer(N)]]` | +| Sampling | `texture(sampler2D, vec2)` | `texture.sample(sampler, float2)` | +| Entry point | `void main()` | `vertex/fragment function_name()` | +| Vector types | `vec2, vec3, vec4` | `float2, float3, float4` | + +## Pasos para Completar el Render Loop + +El método `MetalShader::render()` necesita: + +```objc +1. Obtener drawable del CAMetalLayer: + id drawable = [metal_layer_ nextDrawable]; + +2. Crear command buffer: + id command_buffer = [command_queue_ commandBuffer]; + +3. Crear render pass descriptor: + MTLRenderPassDescriptor* pass_descriptor = [MTLRenderPassDescriptor renderPassDescriptor]; + pass_descriptor.colorAttachments[0].texture = drawable.texture; + pass_descriptor.colorAttachments[0].loadAction = MTLLoadActionClear; + pass_descriptor.colorAttachments[0].storeAction = MTLStoreActionStore; + pass_descriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 1); + +4. Crear render command encoder: + id encoder = + [command_buffer renderCommandEncoderWithDescriptor:pass_descriptor]; + +5. Configurar pipeline y buffers: + [encoder setRenderPipelineState:pipeline_state_]; + [encoder setVertexBuffer:vertex_buffer_ offset:0 atIndex:0]; + [encoder setVertexBuffer:uniforms_buffer_ offset:0 atIndex:1]; + [encoder setFragmentBuffer:uniforms_buffer_ offset:0 atIndex:1]; + [encoder setFragmentTexture:game_texture offset:0 atIndex:0]; + [encoder setFragmentSamplerState:sampler_state_ atIndex:0]; + +6. Dibujar: + [encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle + indexCount:6 + indexType:MTLIndexTypeUInt16 + indexBuffer:index_buffer_ + indexBufferOffset:0]; + +7. Finalizar: + [encoder endEncoding]; + [command_buffer presentDrawable:drawable]; + [command_buffer commit]; +``` + +## Problema Crítico: Obtener MTLTexture desde SDL_Texture + +SDL3 renderiza el juego a `back_buffer_` (SDL_Texture). Necesitamos obtener +la textura Metal subyacente para pasarla al fragment shader. + +**Opciones:** + +1. **SDL_GetProperty()** - Usar SDL3 properties system: + ```cpp + id metal_texture = (__bridge id)SDL_GetProperty( + SDL_GetTextureProperties(back_buffer_), + "SDL.texture.metal.texture", + nullptr + ); + ``` + +2. **Render to Metal texture directamente** - En lugar de usar SDL_Texture, + crear una MTLTexture directamente y renderizar el juego ahí. Más trabajo + pero más control. + +3. **Copiar SDL texture a Metal texture** - Menos eficiente pero más simple. + +## Sampler State + +Falta crear el sampler state (equivalente a glTexParameteri en OpenGL): + +```objc +MTLSamplerDescriptor* sampler_descriptor = [[MTLSamplerDescriptor alloc] init]; +sampler_descriptor.minFilter = MTLSamplerMinMagFilterLinear; +sampler_descriptor.magFilter = MTLSamplerMinMagFilterLinear; +sampler_descriptor.sAddressMode = MTLSamplerAddressModeClampToEdge; +sampler_descriptor.tAddressMode = MTLSamplerAddressModeClampToEdge; + +id sampler_state = [device_ newSamplerStateWithDescriptor:sampler_descriptor]; +``` + +## Build Configuration + +Para compilar en Xcode/CMake, necesitarás: + +1. **CMakeLists.txt** - Añadir metal_shader.mm: + ```cmake + if(APPLE) + set(RENDERING_SOURCES + ${RENDERING_SOURCES} + source/rendering/metal/metal_shader.mm + ) + target_link_libraries(${PROJECT_NAME} + "-framework Metal" + "-framework QuartzCore" + ) + endif() + ``` + +2. **Compilar shaders .metal** - Opcionalmente pre-compilar: + ```bash + xcrun -sdk macosx metal -c crtpi_vertex.metal -o crtpi_vertex.air + xcrun -sdk macosx metal -c crtpi_fragment.metal -o crtpi_fragment.air + xcrun -sdk macosx metallib crtpi_*.air -o crtpi.metallib + ``` + +3. **Cargar .metallib** en código: + ```objc + NSString* path = [[NSBundle mainBundle] pathForResource:@"crtpi" ofType:@"metallib"]; + id library = [device_ newLibraryWithFile:path error:&error]; + ``` + +## Testing en macOS + +Cuando pruebes en macOS: + +1. Verifica que `SDL_WINDOW_METAL` está activo en screen.cpp +2. Compila con `-DCMAKE_BUILD_TYPE=Debug` para ver logs +3. Usa Xcode Instruments (Metal Debugger) para inspeccionar frames +4. Compara rendimiento con/sin shaders + +## Referencias Útiles + +- [Metal Programming Guide](https://developer.apple.com/metal/) +- [Metal Shading Language Specification](https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf) +- [SDL3 Metal Integration](https://github.com/libsdl-org/SDL/blob/main/docs/README-metal.md) + +## Próximos Pasos + +1. Implementar `render()` completo +2. Resolver obtención de textura desde SDL +3. Crear sampler state +4. Testear en macOS real +5. Optimizar si es necesario (probablemente ya será rápido) + +--- + +**Nota importante**: Metal es significativamente más verboso que OpenGL pero +también más eficiente. Una vez que funcione el render loop, el rendimiento +debería ser excelente en macOS. diff --git a/source/rendering/metal/metal_shader.h b/source/rendering/metal/metal_shader.h new file mode 100644 index 0000000..78b8abe --- /dev/null +++ b/source/rendering/metal/metal_shader.h @@ -0,0 +1,76 @@ +#pragma once + +#include "../shader_backend.h" + +#ifdef __APPLE__ + +#import +#import + +namespace Rendering { + +/** + * @brief Backend de shaders usando Metal para macOS + * + * Implementa el renderizado de shaders usando Metal API nativo de Apple: + * - MTLDevice para acceso a GPU + * - MTLRenderPipelineState para configuración del pipeline + * - MTLCommandQueue para enviar comandos a GPU + * - MSL (Metal Shading Language) para shaders + */ +class MetalShader : public ShaderBackend { +public: + MetalShader() = default; + ~MetalShader() override; + + bool init(SDL_Window* window, + SDL_Texture* texture, + const std::string& vertex_source, + const std::string& fragment_source) override; + + void render() override; + void setTextureSize(float width, float height) override; + void cleanup() override; + bool isHardwareAccelerated() const override { return is_initialized_; } + +private: + // Funciones auxiliares + bool createMetalDevice(); + bool compileShaders(const std::string& vertex_source, + const std::string& fragment_source); + bool createPipeline(); + bool createBuffers(); + id getMetalTexture(SDL_Texture* texture); + + // Estado SDL + SDL_Window* window_ = nullptr; + SDL_Renderer* renderer_ = nullptr; + SDL_Texture* back_buffer_ = nullptr; + + // Estado Metal + id device_ = nil; // GPU device + id command_queue_ = nil; // Cola de comandos + id pipeline_state_ = nil; // Estado del pipeline + id vertex_buffer_ = nil; // Buffer de vértices + id index_buffer_ = nil; // Buffer de índices + id uniforms_buffer_ = nil; // Buffer de uniforms + CAMetalLayer* metal_layer_ = nil; // Layer de Metal + + // Uniforms + struct Uniforms { + float textureSize[2]; // width, height + } uniforms_; + + // Tamaños + int window_width_ = 0; + int window_height_ = 0; + float texture_width_ = 0.0f; + float texture_height_ = 0.0f; + + // Estado + bool is_initialized_ = false; +}; + +} // namespace Rendering + +#endif // __APPLE__ diff --git a/source/rendering/metal/metal_shader.mm b/source/rendering/metal/metal_shader.mm new file mode 100644 index 0000000..926bd5e --- /dev/null +++ b/source/rendering/metal/metal_shader.mm @@ -0,0 +1,301 @@ +// Usar .mm (Objective-C++) en lugar de .cpp para código Metal en macOS + +#include "metal_shader.h" + +#ifdef __APPLE__ + +#import +#import +#include + +namespace Rendering { + +MetalShader::~MetalShader() { + cleanup(); +} + +bool MetalShader::createMetalDevice() { + // Obtener el device Metal por defecto (GPU del sistema) + device_ = MTLCreateSystemDefaultDevice(); + if (!device_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "ERROR: No se pudo crear Metal device"); + return false; + } + + // Crear command queue + command_queue_ = [device_ newCommandQueue]; + if (!command_queue_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "ERROR: No se pudo crear Metal command queue"); + return false; + } + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Metal device creado: %s", [[device_ name] UTF8String]); + + return true; +} + +bool MetalShader::compileShaders(const std::string& vertex_source, + const std::string& fragment_source) { + // En Metal, los shaders se pueden compilar de dos formas: + // 1. Runtime compilation desde strings (lo que haremos aquí) + // 2. Pre-compilados a .metallib (más eficiente pero requiere build step) + + NSError* error = nil; + + // Convertir sources a NSString + NSString* vertex_code = [NSString stringWithUTF8String:vertex_source.c_str()]; + NSString* fragment_code = [NSString stringWithUTF8String:fragment_source.c_str()]; + + // Combinar vertex + fragment en un solo source (Metal permite múltiples shaders por library) + NSString* combined_source = [NSString stringWithFormat:@"%@\n\n%@", + vertex_code, fragment_code]; + + // Crear library desde source code + id library = [device_ newLibraryWithSource:combined_source + options:nil + error:&error]; + if (!library) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "ERROR: Fallo al compilar shaders Metal: %s", + [[error localizedDescription] UTF8String]); + return false; + } + + // Obtener funciones del library + id vertex_function = [library newFunctionWithName:@"vertex_main"]; + id fragment_function = [library newFunctionWithName:@"fragment_main"]; + + if (!vertex_function || !fragment_function) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "ERROR: No se encontraron las funciones vertex_main o fragment_main"); + return false; + } + + // Crear pipeline descriptor + MTLRenderPipelineDescriptor* pipeline_descriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipeline_descriptor.vertexFunction = vertex_function; + pipeline_descriptor.fragmentFunction = fragment_function; + + // Configurar formato de color (BGRA8Unorm es común en macOS) + pipeline_descriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; + + // Configurar vertex descriptor (cómo están organizados los vértices) + MTLVertexDescriptor* vertex_descriptor = [[MTLVertexDescriptor alloc] init]; + + // Atributo 0: position (2 floats) + vertex_descriptor.attributes[0].format = MTLVertexFormatFloat2; + vertex_descriptor.attributes[0].offset = 0; + vertex_descriptor.attributes[0].bufferIndex = 0; + + // Atributo 1: texCoord (2 floats) + vertex_descriptor.attributes[1].format = MTLVertexFormatFloat2; + vertex_descriptor.attributes[1].offset = 2 * sizeof(float); + vertex_descriptor.attributes[1].bufferIndex = 0; + + // Layout del buffer (stride = 4 floats por vértice) + vertex_descriptor.layouts[0].stride = 4 * sizeof(float); + vertex_descriptor.layouts[0].stepRate = 1; + vertex_descriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; + + pipeline_descriptor.vertexDescriptor = vertex_descriptor; + + // Crear pipeline state + pipeline_state_ = [device_ newRenderPipelineStateWithDescriptor:pipeline_descriptor + error:&error]; + if (!pipeline_state_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "ERROR: Fallo al crear pipeline state: %s", + [[error localizedDescription] UTF8String]); + return false; + } + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Shaders Metal compilados correctamente"); + + return true; +} + +bool MetalShader::createBuffers() { + // Crear quad de pantalla completa (igual que en OpenGL) + // Formato: x, y, u, v + float vertices[] = { + -1.0f, -1.0f, 0.0f, 0.0f, // Inferior izquierda + 1.0f, -1.0f, 1.0f, 0.0f, // Inferior derecha + 1.0f, 1.0f, 1.0f, 1.0f, // Superior derecha + -1.0f, 1.0f, 0.0f, 1.0f // Superior izquierda + }; + + uint16_t indices[] = { + 0, 1, 2, // Primer triángulo + 2, 3, 0 // Segundo triángulo + }; + + // Crear vertex buffer + vertex_buffer_ = [device_ newBufferWithBytes:vertices + length:sizeof(vertices) + options:MTLResourceStorageModeShared]; + if (!vertex_buffer_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "ERROR: Fallo al crear vertex buffer"); + return false; + } + + // Crear index buffer + index_buffer_ = [device_ newBufferWithBytes:indices + length:sizeof(indices) + options:MTLResourceStorageModeShared]; + if (!index_buffer_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "ERROR: Fallo al crear index buffer"); + return false; + } + + // Crear uniforms buffer + uniforms_buffer_ = [device_ newBufferWithLength:sizeof(Uniforms) + options:MTLResourceStorageModeShared]; + if (!uniforms_buffer_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "ERROR: Fallo al crear uniforms buffer"); + return false; + } + + return true; +} + +bool MetalShader::init(SDL_Window* window, + SDL_Texture* texture, + const std::string& vertex_source, + const std::string& fragment_source) { + window_ = window; + back_buffer_ = texture; + renderer_ = SDL_GetRenderer(window); + + if (!renderer_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "ERROR: No se pudo obtener el renderer"); + return false; + } + + // Verificar que es Metal renderer + const char* renderer_name = SDL_GetRendererName(renderer_); + if (!renderer_name || strncmp(renderer_name, "metal", 5) != 0) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Renderer no es Metal: %s", renderer_name ? renderer_name : "unknown"); + return false; + } + + // Obtener tamaños + SDL_GetWindowSize(window_, &window_width_, &window_height_); + SDL_GetTextureSize(back_buffer_, &texture_width_, &texture_height_); + + // Crear Metal device + if (!createMetalDevice()) { + return false; + } + + // Compilar shaders + if (!compileShaders(vertex_source, fragment_source)) { + return false; + } + + // Crear buffers + if (!createBuffers()) { + return false; + } + + // Inicializar uniforms + uniforms_.textureSize[0] = texture_width_; + uniforms_.textureSize[1] = texture_height_; + + // Copiar uniforms al buffer + memcpy([uniforms_buffer_ contents], &uniforms_, sizeof(Uniforms)); + + // Obtener Metal layer de SDL + metal_layer_ = (__bridge CAMetalLayer*)SDL_GetProperty( + SDL_GetWindowProperties(window_), + SDL_PROP_WINDOW_METAL_LAYER_POINTER, + nullptr + ); + + if (!metal_layer_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "ERROR: No se pudo obtener Metal layer"); + return false; + } + + metal_layer_.device = device_; + metal_layer_.pixelFormat = MTLPixelFormatBGRA8Unorm; + + is_initialized_ = true; + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "** Metal Shader Backend inicializado correctamente"); + + return true; +} + +void MetalShader::render() { + if (!is_initialized_ || !pipeline_state_) { + // Fallback a renderizado normal + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); + SDL_SetRenderTarget(renderer_, nullptr); + SDL_RenderClear(renderer_); + SDL_RenderTexture(renderer_, back_buffer_, nullptr, nullptr); + SDL_RenderPresent(renderer_); + return; + } + + // TODO: Implementar render loop de Metal + // 1. Obtener drawable del layer + // 2. Crear command buffer + // 3. Crear render pass descriptor + // 4. Encodear comandos de dibujo + // 5. Commit y presentar + + // NOTA: Esta es la parte más compleja y requiere más trabajo + // Por ahora dejamos un stub para que compile + + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Metal render() no implementado completamente todavía"); +} + +void MetalShader::setTextureSize(float width, float height) { + if (!is_initialized_) { + return; + } + + texture_width_ = width; + texture_height_ = height; + + // Actualizar uniforms + uniforms_.textureSize[0] = width; + uniforms_.textureSize[1] = height; + + memcpy([uniforms_buffer_ contents], &uniforms_, sizeof(Uniforms)); +} + +void MetalShader::cleanup() { + // En Objective-C++ con ARC, no necesitamos release manual + // pero limpiamos las referencias + pipeline_state_ = nil; + command_queue_ = nil; + vertex_buffer_ = nil; + index_buffer_ = nil; + uniforms_buffer_ = nil; + device_ = nil; + metal_layer_ = nil; + + is_initialized_ = false; +} + +id MetalShader::getMetalTexture(SDL_Texture* texture) { + // TODO: Obtener la textura Metal desde SDL_Texture + // Esto requiere acceder a las properties de SDL3 + return nil; +} + +} // namespace Rendering + +#endif // __APPLE__ diff --git a/source/screen.cpp b/source/screen.cpp index 35090ce..1d56e82 100644 --- a/source/screen.cpp +++ b/source/screen.cpp @@ -11,6 +11,9 @@ #include "asset.h" // Para Asset #include "mouse.h" // Para updateCursorVisibility #include "rendering/opengl/opengl_shader.h" // Para OpenGLShader +#ifdef __APPLE__ +// #include "rendering/metal/metal_shader.h" // Para MetalShader (TODO: descomentar cuando esté completo) +#endif #include "options.h" // Para VideoOptions, video, WindowOptions, window #include "param.h" // Para Param, param, ParamGame, ParamDebug #include "text.h" // Para Text, Text::COLOR, Text::STROKE