WIP: Metal shader backend para macOS

- 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.
This commit is contained in:
2025-10-02 21:05:28 +02:00
parent ff7aef827c
commit d6ffbda00d
8 changed files with 762 additions and 1 deletions

View File

@@ -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<CAMetalDrawable> drawable = [metal_layer_ nextDrawable];
2. Crear command buffer:
id<MTLCommandBuffer> 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<MTLRenderCommandEncoder> 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<MTLTexture> metal_texture = (__bridge id<MTLTexture>)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<MTLSamplerState> 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<MTLLibrary> 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.

View File

@@ -0,0 +1,76 @@
#pragma once
#include "../shader_backend.h"
#ifdef __APPLE__
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
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<MTLTexture> getMetalTexture(SDL_Texture* texture);
// Estado SDL
SDL_Window* window_ = nullptr;
SDL_Renderer* renderer_ = nullptr;
SDL_Texture* back_buffer_ = nullptr;
// Estado Metal
id<MTLDevice> device_ = nil; // GPU device
id<MTLCommandQueue> command_queue_ = nil; // Cola de comandos
id<MTLRenderPipelineState> pipeline_state_ = nil; // Estado del pipeline
id<MTLBuffer> vertex_buffer_ = nil; // Buffer de vértices
id<MTLBuffer> index_buffer_ = nil; // Buffer de índices
id<MTLBuffer> 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__

View File

@@ -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 <SDL3/SDL.h>
#import <SDL3/SDL_metal.h>
#include <vector>
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<MTLLibrary> 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<MTLFunction> vertex_function = [library newFunctionWithName:@"vertex_main"];
id<MTLFunction> 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<MTLTexture> 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__

View File

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