Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6ffbda00d |
@@ -120,6 +120,14 @@ set(RENDERING_SOURCES
|
|||||||
source/rendering/opengl/opengl_shader.cpp
|
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
|
# Configuración de SDL3
|
||||||
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
||||||
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
||||||
@@ -159,6 +167,11 @@ elseif(APPLE)
|
|||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUILD)
|
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUILD)
|
||||||
target_compile_options(${PROJECT_NAME} PRIVATE -Wno-deprecated)
|
target_compile_options(${PROJECT_NAME} PRIVATE -Wno-deprecated)
|
||||||
set(CMAKE_OSX_ARCHITECTURES "arm64")
|
set(CMAKE_OSX_ARCHITECTURES "arm64")
|
||||||
|
# Enlazar frameworks de Metal
|
||||||
|
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||||
|
"-framework Metal"
|
||||||
|
"-framework QuartzCore"
|
||||||
|
)
|
||||||
elseif(UNIX AND NOT APPLE)
|
elseif(UNIX AND NOT APPLE)
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
|
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -74,10 +74,14 @@ SOUND|${PREFIX}/data/sound/voice_recover.wav
|
|||||||
SOUND|${PREFIX}/data/sound/voice_thankyou.wav
|
SOUND|${PREFIX}/data/sound/voice_thankyou.wav
|
||||||
SOUND|${PREFIX}/data/sound/walk.wav
|
SOUND|${PREFIX}/data/sound/walk.wav
|
||||||
|
|
||||||
# Shaders
|
# Shaders OpenGL (Windows/Linux)
|
||||||
DATA|${PREFIX}/data/shaders/crtpi_vertex.glsl
|
DATA|${PREFIX}/data/shaders/crtpi_vertex.glsl
|
||||||
DATA|${PREFIX}/data/shaders/crtpi_fragment.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
|
# Texturas - Balloons
|
||||||
ANIMATION|${PREFIX}/data/gfx/balloon/balloon0.ani
|
ANIMATION|${PREFIX}/data/gfx/balloon/balloon0.ani
|
||||||
ANIMATION|${PREFIX}/data/gfx/balloon/balloon1.ani
|
ANIMATION|${PREFIX}/data/gfx/balloon/balloon1.ani
|
||||||
|
|||||||
152
data/shaders/crtpi_fragment.metal
Normal file
152
data/shaders/crtpi_fragment.metal
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
//
|
||||||
|
// CRT-Pi Fragment Shader - Metal Shading Language
|
||||||
|
// Portado desde GLSL a MSL para macOS
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <metal_stdlib>
|
||||||
|
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<float> 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);
|
||||||
|
}
|
||||||
46
data/shaders/crtpi_vertex.metal
Normal file
46
data/shaders/crtpi_vertex.metal
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// CRT-Pi Vertex Shader - Metal Shading Language
|
||||||
|
// Portado desde GLSL a MSL para macOS
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <metal_stdlib>
|
||||||
|
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;
|
||||||
|
}
|
||||||
166
source/rendering/metal/METAL_BACKEND_NOTES.md
Normal file
166
source/rendering/metal/METAL_BACKEND_NOTES.md
Normal 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.
|
||||||
76
source/rendering/metal/metal_shader.h
Normal file
76
source/rendering/metal/metal_shader.h
Normal 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__
|
||||||
301
source/rendering/metal/metal_shader.mm
Normal file
301
source/rendering/metal/metal_shader.mm
Normal 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__
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
#include "asset.h" // Para Asset
|
#include "asset.h" // Para Asset
|
||||||
#include "mouse.h" // Para updateCursorVisibility
|
#include "mouse.h" // Para updateCursorVisibility
|
||||||
#include "rendering/opengl/opengl_shader.h" // Para OpenGLShader
|
#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 "options.h" // Para VideoOptions, video, WindowOptions, window
|
||||||
#include "param.h" // Para Param, param, ParamGame, ParamDebug
|
#include "param.h" // Para Param, param, ParamGame, ParamDebug
|
||||||
#include "text.h" // Para Text, Text::COLOR, Text::STROKE
|
#include "text.h" // Para Text, Text::COLOR, Text::STROKE
|
||||||
|
|||||||
Reference in New Issue
Block a user