From c6c4aebab187e580c3220f2e877a272ba01e8f9c Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 10 Sep 2025 23:14:18 +0200 Subject: [PATCH] treballant en metal --- CMakeLists.txt | 22 ++ config/assets.txt | 2 + data/shaders/crtpi_240.metal | 185 +++++++++++++++++ data/shaders/crtpi_256.metal | 185 +++++++++++++++++ source/external/jail_shader.cpp | 45 +++- source/external/jail_shader.h | 14 +- source/external/jail_shader_metal.mm | 293 +++++++++++++++++++++++++++ source/screen.cpp | 14 ++ 8 files changed, 751 insertions(+), 9 deletions(-) create mode 100644 data/shaders/crtpi_240.metal create mode 100644 data/shaders/crtpi_256.metal create mode 100644 source/external/jail_shader_metal.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index b4f355f..c6bb465 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,11 @@ set(EXTERNAL_SOURCES source/external/gif.cpp ) +# Añadir archivos específicos para macOS (Metal) +if(APPLE) + list(APPEND EXTERNAL_SOURCES source/external/jail_shader_metal.mm) +endif() + # Añadir jail_audio.cpp solo si el audio está habilitado if(NOT DISABLE_AUDIO) list(APPEND EXTERNAL_SOURCES source/external/jail_audio.cpp) @@ -147,6 +152,23 @@ 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 para macOS + find_library(METAL_FRAMEWORK Metal) + find_library(QUARTZCORE_FRAMEWORK QuartzCore) + find_library(COREFOUNDATION_FRAMEWORK CoreFoundation) + find_library(FOUNDATION_FRAMEWORK Foundation) + + if(METAL_FRAMEWORK AND QUARTZCORE_FRAMEWORK AND COREFOUNDATION_FRAMEWORK AND FOUNDATION_FRAMEWORK) + target_link_libraries(${PROJECT_NAME} PRIVATE + ${METAL_FRAMEWORK} + ${QUARTZCORE_FRAMEWORK} + ${COREFOUNDATION_FRAMEWORK} + ${FOUNDATION_FRAMEWORK}) + message(STATUS "Metal frameworks encontrados y enlazados") + else() + message(WARNING "Algunos frameworks de Metal no se encontraron - OpenGL será usado como fallback") + endif() 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 d6bd06a..558e692 100644 --- a/config/assets.txt +++ b/config/assets.txt @@ -71,7 +71,9 @@ SOUND|${PREFIX}/data/sound/walk.wav # Shaders DATA|${PREFIX}/data/shaders/crtpi_240.glsl +DATA|${PREFIX}/data/shaders/crtpi_240.metal DATA|${PREFIX}/data/shaders/crtpi_256.glsl +DATA|${PREFIX}/data/shaders/crtpi_256.metal # Texturas - Balloons ANIMATION|${PREFIX}/data/gfx/balloon/balloon0.ani diff --git a/data/shaders/crtpi_240.metal b/data/shaders/crtpi_240.metal new file mode 100644 index 0000000..66f7575 --- /dev/null +++ b/data/shaders/crtpi_240.metal @@ -0,0 +1,185 @@ +/* + crt-pi - A Raspberry Pi friendly CRT shader. + + Metal Shading Language version converted from GLSL + Copyright (C) 2015-2016 davej + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. +*/ + +#include +using namespace metal; + +// Haven't put these as parameters as it would slow the code down. +#define SCANLINES +#define MULTISAMPLE +#define GAMMA +//#define FAKE_GAMMA +//#define CURVATURE +//#define SHARPER +// MASK_TYPE: 0 = none, 1 = green/magenta, 2 = trinitron(ish) +#define MASK_TYPE 2 + +// Constants +#define CURVATURE_X 0.05 +#define CURVATURE_Y 0.1 +#define MASK_BRIGHTNESS 0.80 +#define SCANLINE_WEIGHT 6.0 +#define SCANLINE_GAP_BRIGHTNESS 0.12 +#define BLOOM_FACTOR 3.5 +#define INPUT_GAMMA 2.4 +#define OUTPUT_GAMMA 2.2 + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texcoord [[attribute(1)]]; +}; + +struct VertexOut { + float4 position [[position]]; + float2 TEX0; +#if defined(CURVATURE) + float2 screenScale; +#endif + float filterWidth; +}; + +vertex VertexOut vertex_main(VertexIn in [[stage_in]]) { + VertexOut out; + +#if defined(CURVATURE) + out.screenScale = float2(1.0, 1.0); +#endif + out.filterWidth = (768.0 / 240.0) / 3.0; + out.TEX0 = float2(in.texcoord.x, 1.0 - in.texcoord.y) * 1.0001; + out.position = in.position; + + return out; +} + +#if defined(CURVATURE) +float2 Distort(float2 coord, float2 screenScale) { + float2 CURVATURE_DISTORTION = float2(CURVATURE_X, CURVATURE_Y); + // Barrel distortion shrinks the display area a bit, this will allow us to counteract that. + float2 barrelScale = 1.0 - (0.23 * CURVATURE_DISTORTION); + coord *= screenScale; + coord -= float2(0.5); + float rsq = coord.x * coord.x + coord.y * coord.y; + coord += coord * (CURVATURE_DISTORTION * rsq); + coord *= barrelScale; + if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) + coord = float2(-1.0); // If out of bounds, return an invalid value. + else { + coord += float2(0.5); + coord /= screenScale; + } + + return coord; +} +#endif + +float CalcScanLineWeight(float dist) { + return max(1.0 - dist * dist * SCANLINE_WEIGHT, SCANLINE_GAP_BRIGHTNESS); +} + +float CalcScanLine(float dy, float filterWidth) { + float scanLineWeight = CalcScanLineWeight(dy); +#if defined(MULTISAMPLE) + scanLineWeight += CalcScanLineWeight(dy - filterWidth); + scanLineWeight += CalcScanLineWeight(dy + filterWidth); + scanLineWeight *= 0.3333333; +#endif + return scanLineWeight; +} + +fragment float4 fragment_main(VertexOut in [[stage_in]], + texture2d Texture [[texture(0)]], + sampler textureSampler [[sampler(0)]]) { + float2 TextureSize = float2(320.0, 240.0); +#if defined(CURVATURE) + float2 texcoord = Distort(in.TEX0, in.screenScale); + if (texcoord.x < 0.0) + return float4(0.0); + else +#else + float2 texcoord = in.TEX0; +#endif + { + float2 texcoordInPixels = texcoord * TextureSize; +#if defined(SHARPER) + float2 tempCoord = floor(texcoordInPixels) + 0.5; + float2 coord = tempCoord / TextureSize; + float2 deltas = texcoordInPixels - tempCoord; + float 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 /= TextureSize; + deltas *= signs; + float2 tc = coord + deltas; +#else + float tempY = floor(texcoordInPixels.y) + 0.5; + float yCoord = tempY / TextureSize.y; + float dy = texcoordInPixels.y - tempY; + float scanLineWeight = CalcScanLine(dy, in.filterWidth); + float signY = sign(dy); + dy = dy * dy; + dy = dy * dy; + dy *= 8.0; + dy /= TextureSize.y; + dy *= signY; + float2 tc = float2(texcoord.x, yCoord + dy); +#endif + + float3 colour = Texture.sample(textureSampler, tc).rgb; + +#if defined(SCANLINES) +#if defined(GAMMA) +#if defined(FAKE_GAMMA) + colour = colour * colour; +#else + colour = pow(colour, float3(INPUT_GAMMA)); +#endif +#endif + scanLineWeight *= BLOOM_FACTOR; + colour *= scanLineWeight; + +#if defined(GAMMA) +#if defined(FAKE_GAMMA) + colour = sqrt(colour); +#else + colour = pow(colour, float3(1.0/OUTPUT_GAMMA)); +#endif +#endif +#endif +#if MASK_TYPE == 0 + return float4(colour, 1.0); +#else +#if MASK_TYPE == 1 + float whichMask = fract((in.position.x * 1.0001) * 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); +#elif MASK_TYPE == 2 + float whichMask = fract((in.position.x * 1.0001) * 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; +#endif + + return float4(colour * mask, 1.0); +#endif + } +} \ No newline at end of file diff --git a/data/shaders/crtpi_256.metal b/data/shaders/crtpi_256.metal new file mode 100644 index 0000000..20bd323 --- /dev/null +++ b/data/shaders/crtpi_256.metal @@ -0,0 +1,185 @@ +/* + crt-pi - A Raspberry Pi friendly CRT shader. + + Metal Shading Language version converted from GLSL + Copyright (C) 2015-2016 davej + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. +*/ + +#include +using namespace metal; + +// Haven't put these as parameters as it would slow the code down. +#define SCANLINES +#define MULTISAMPLE +#define GAMMA +//#define FAKE_GAMMA +//#define CURVATURE +//#define SHARPER +// MASK_TYPE: 0 = none, 1 = green/magenta, 2 = trinitron(ish) +#define MASK_TYPE 2 + +// Constants +#define CURVATURE_X 0.05 +#define CURVATURE_Y 0.1 +#define MASK_BRIGHTNESS 0.80 +#define SCANLINE_WEIGHT 6.0 +#define SCANLINE_GAP_BRIGHTNESS 0.12 +#define BLOOM_FACTOR 3.5 +#define INPUT_GAMMA 2.4 +#define OUTPUT_GAMMA 2.2 + +struct VertexIn { + float4 position [[attribute(0)]]; + float2 texcoord [[attribute(1)]]; +}; + +struct VertexOut { + float4 position [[position]]; + float2 TEX0; +#if defined(CURVATURE) + float2 screenScale; +#endif + float filterWidth; +}; + +vertex VertexOut vertex_main(VertexIn in [[stage_in]]) { + VertexOut out; + +#if defined(CURVATURE) + out.screenScale = float2(1.0, 1.0); +#endif + out.filterWidth = (768.0 / 256.0) / 3.0; + out.TEX0 = float2(in.texcoord.x, 1.0 - in.texcoord.y) * 1.0001; + out.position = in.position; + + return out; +} + +#if defined(CURVATURE) +float2 Distort(float2 coord, float2 screenScale) { + float2 CURVATURE_DISTORTION = float2(CURVATURE_X, CURVATURE_Y); + // Barrel distortion shrinks the display area a bit, this will allow us to counteract that. + float2 barrelScale = 1.0 - (0.23 * CURVATURE_DISTORTION); + coord *= screenScale; + coord -= float2(0.5); + float rsq = coord.x * coord.x + coord.y * coord.y; + coord += coord * (CURVATURE_DISTORTION * rsq); + coord *= barrelScale; + if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) + coord = float2(-1.0); // If out of bounds, return an invalid value. + else { + coord += float2(0.5); + coord /= screenScale; + } + + return coord; +} +#endif + +float CalcScanLineWeight(float dist) { + return max(1.0 - dist * dist * SCANLINE_WEIGHT, SCANLINE_GAP_BRIGHTNESS); +} + +float CalcScanLine(float dy, float filterWidth) { + float scanLineWeight = CalcScanLineWeight(dy); +#if defined(MULTISAMPLE) + scanLineWeight += CalcScanLineWeight(dy - filterWidth); + scanLineWeight += CalcScanLineWeight(dy + filterWidth); + scanLineWeight *= 0.3333333; +#endif + return scanLineWeight; +} + +fragment float4 fragment_main(VertexOut in [[stage_in]], + texture2d Texture [[texture(0)]], + sampler textureSampler [[sampler(0)]]) { + float2 TextureSize = float2(320.0, 256.0); +#if defined(CURVATURE) + float2 texcoord = Distort(in.TEX0, in.screenScale); + if (texcoord.x < 0.0) + return float4(0.0); + else +#else + float2 texcoord = in.TEX0; +#endif + { + float2 texcoordInPixels = texcoord * TextureSize; +#if defined(SHARPER) + float2 tempCoord = floor(texcoordInPixels) + 0.5; + float2 coord = tempCoord / TextureSize; + float2 deltas = texcoordInPixels - tempCoord; + float 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 /= TextureSize; + deltas *= signs; + float2 tc = coord + deltas; +#else + float tempY = floor(texcoordInPixels.y) + 0.5; + float yCoord = tempY / TextureSize.y; + float dy = texcoordInPixels.y - tempY; + float scanLineWeight = CalcScanLine(dy, in.filterWidth); + float signY = sign(dy); + dy = dy * dy; + dy = dy * dy; + dy *= 8.0; + dy /= TextureSize.y; + dy *= signY; + float2 tc = float2(texcoord.x, yCoord + dy); +#endif + + float3 colour = Texture.sample(textureSampler, tc).rgb; + +#if defined(SCANLINES) +#if defined(GAMMA) +#if defined(FAKE_GAMMA) + colour = colour * colour; +#else + colour = pow(colour, float3(INPUT_GAMMA)); +#endif +#endif + scanLineWeight *= BLOOM_FACTOR; + colour *= scanLineWeight; + +#if defined(GAMMA) +#if defined(FAKE_GAMMA) + colour = sqrt(colour); +#else + colour = pow(colour, float3(1.0/OUTPUT_GAMMA)); +#endif +#endif +#endif +#if MASK_TYPE == 0 + return float4(colour, 1.0); +#else +#if MASK_TYPE == 1 + float whichMask = fract((in.position.x * 1.0001) * 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); +#elif MASK_TYPE == 2 + float whichMask = fract((in.position.x * 1.0001) * 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; +#endif + + return float4(colour * mask, 1.0); +#endif + } +} \ No newline at end of file diff --git a/source/external/jail_shader.cpp b/source/external/jail_shader.cpp index fe49e5a..b5375b2 100644 --- a/source/external/jail_shader.cpp +++ b/source/external/jail_shader.cpp @@ -234,7 +234,7 @@ GLuint getTextureID(SDL_Texture *texture) { return textureId; } -bool init(SDL_Window *window, SDL_Texture *back_buffer_texture, const std::string &vertex_shader, const std::string &fragment_shader) { +bool init(SDL_Window *window, SDL_Texture *back_buffer_texture, const std::string &shader_source, const std::string &fragment_shader) { shader::win = window; shader::renderer = SDL_GetRenderer(window); shader::backBuffer = back_buffer_texture; @@ -253,7 +253,15 @@ bool init(SDL_Window *window, SDL_Texture *back_buffer_texture, const std::strin return false; } - // Verificar que el renderer sea OpenGL +#ifdef __APPLE__ + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected renderer: %s", render_name); + + // En macOS, SIEMPRE usar OpenGL shaders, incluso con Metal renderer SDL + // Esto nos da lo mejor de ambos: Metal para SDL (rendimiento) + OpenGL para shaders CRT + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "macOS: Using OpenGL shaders with %s SDL renderer", render_name); +#endif + + // Verificar que el renderer sea OpenGL solamente (Metal no puede usar shaders OpenGL) if (!strncmp(render_name, "opengl", 6)) { #ifndef __APPLE__ if (!initGLExtensions()) { @@ -263,24 +271,39 @@ bool init(SDL_Window *window, SDL_Texture *back_buffer_texture, const std::strin } #endif // Compilar el programa de shaders utilizando std::string - programId = compileProgram(vertex_shader, fragment_shader); + programId = compileProgram(shader_source, fragment_shader); if (programId == INVALID_PROGRAM_ID) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ERROR: No se pudo compilar el programa de shaders."); usingOpenGL = false; return false; } + + usingOpenGL = true; + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "** OpenGL shader system initialized successfully"); + return true; + } else if (!strncmp(render_name, "metal", 5)) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "SDL using Metal renderer - CRT shaders not available yet"); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Running with excellent Metal performance (~450+ FPS) without CRT effects"); + usingOpenGL = false; + return true; // Success, but no shaders } else { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "ADVERTENCIA: El driver del renderer no es OpenGL (%s).", render_name); + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "ADVERTENCIA: El driver del renderer no es compatible (%s).", render_name); usingOpenGL = false; return false; } - - usingOpenGL = true; - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "** Shader system initialized successfully"); - return true; } void render() { +#ifdef __APPLE__ + // En macOS con Metal, los shaders están deshabilitados por ahora + // TODO: Implementar integración correcta SDL Metal + shaders CRT + if (!usingOpenGL) { + // Usar renderizado SDL normal (sin shaders) + // Esto da excelente rendimiento ~450+ FPS + return; // Let SDL handle rendering normally + } +#endif + // Establece el color de fondo SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_SetRenderTarget(renderer, nullptr); @@ -392,6 +415,12 @@ void render() { } void cleanup() { +#ifdef __APPLE__ + if (!usingOpenGL) { + metal::cleanupMetal(); + } +#endif + if (programId != INVALID_PROGRAM_ID) { glDeleteProgram(programId); programId = INVALID_PROGRAM_ID; diff --git a/source/external/jail_shader.h b/source/external/jail_shader.h index 47aad0f..f2bb5bd 100644 --- a/source/external/jail_shader.h +++ b/source/external/jail_shader.h @@ -4,6 +4,18 @@ #include // Para basic_string, string namespace shader { -bool init(SDL_Window *ventana, SDL_Texture *texturaBackBuffer, const std::string &vertexShader, const std::string &fragmentShader = ""); +bool init(SDL_Window *ventana, SDL_Texture *texturaBackBuffer, const std::string &shaderSource, const std::string &fragmentShader = ""); void render(); +void cleanup(); +bool isUsingOpenGL(); + +#ifdef __APPLE__ +namespace metal { +bool initMetal(SDL_Window* window, SDL_Texture* backBuffer, const std::string& shaderFilename); +void updateMetalTexture(SDL_Texture* backBuffer); +void renderMetal(); +void cleanupMetal(); +} +#endif + } // namespace shader \ No newline at end of file diff --git a/source/external/jail_shader_metal.mm b/source/external/jail_shader_metal.mm new file mode 100644 index 0000000..3c6a4cb --- /dev/null +++ b/source/external/jail_shader_metal.mm @@ -0,0 +1,293 @@ +#include "jail_shader.h" + +#ifdef __APPLE__ +#include +#include +#include +#include +#include +#include +#include +#include "../asset.h" + +namespace shader { +namespace metal { + +// Metal objects +id device = nullptr; +id commandQueue = nullptr; +id pipelineState = nullptr; +id metalTexture = nullptr; +id vertexBuffer = nullptr; +CAMetalLayer* metalLayer = nullptr; +SDL_MetalView metalView = nullptr; + +// Vertex data for fullscreen quad +struct Vertex { + float position[4]; // x, y, z, w + float texcoord[2]; // u, v +}; + +const Vertex quadVertices[] = { + // Position (x, y, z, w) // TexCoord (u, v) + {{-1.0f, -1.0f, 0.0f, 1.0f}, {0.0f, 1.0f}}, // Bottom-left + {{ 1.0f, -1.0f, 0.0f, 1.0f}, {1.0f, 1.0f}}, // Bottom-right + {{-1.0f, 1.0f, 0.0f, 1.0f}, {0.0f, 0.0f}}, // Top-left + {{ 1.0f, 1.0f, 0.0f, 1.0f}, {1.0f, 0.0f}}, // Top-right +}; + +std::string loadMetalShader(const std::string& filename) { + // Try to load the .metal file from the same location as GLSL files + auto data = Asset::get()->loadData(filename); + if (!data.empty()) { + return std::string(data.begin(), data.end()); + } + return ""; +} + +bool initMetal(SDL_Window* window, SDL_Texture* backBuffer, const std::string& shaderFilename) { + // Get Metal view from SDL + metalView = SDL_Metal_CreateView(window); + if (!metalView) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create Metal view: %s", SDL_GetError()); + return false; + } + + metalLayer = (__bridge CAMetalLayer*)SDL_Metal_GetLayer(metalView); + if (!metalLayer) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get Metal layer"); + return false; + } + + // Get Metal device + device = MTLCreateSystemDefaultDevice(); + if (!device) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create Metal device"); + return false; + } + + // Configure the layer + metalLayer.device = device; + metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; + metalLayer.displaySyncEnabled = YES; + + // Create command queue + commandQueue = [device newCommandQueue]; + if (!commandQueue) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create Metal command queue"); + return false; + } + + // Load and compile shaders + std::string metalShaderSource = loadMetalShader(shaderFilename); + if (metalShaderSource.empty()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load Metal shader: %s", shaderFilename.c_str()); + return false; + } + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded Metal shader %s (length: %zu)", + shaderFilename.c_str(), metalShaderSource.length()); + + NSString* shaderNSString = [NSString stringWithUTF8String:metalShaderSource.c_str()]; + NSError* error = nil; + id library = [device newLibraryWithSource:shaderNSString options:nil error:&error]; + if (!library || error) { + if (error) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to compile Metal shader: %s", + [[error localizedDescription] UTF8String]); + } + return false; + } + + id vertexFunction = [library newFunctionWithName:@"vertex_main"]; + id fragmentFunction = [library newFunctionWithName:@"fragment_main"]; + + if (!vertexFunction || !fragmentFunction) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load Metal shader functions"); + return false; + } + + // Create render pipeline + MTLRenderPipelineDescriptor* pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipelineDescriptor.vertexFunction = vertexFunction; + pipelineDescriptor.fragmentFunction = fragmentFunction; + pipelineDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat; + + // Set up vertex descriptor + MTLVertexDescriptor* vertexDescriptor = [[MTLVertexDescriptor alloc] init]; + vertexDescriptor.attributes[0].format = MTLVertexFormatFloat4; + vertexDescriptor.attributes[0].offset = 0; + vertexDescriptor.attributes[0].bufferIndex = 0; + vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; + vertexDescriptor.attributes[1].offset = 4 * sizeof(float); + vertexDescriptor.attributes[1].bufferIndex = 0; + vertexDescriptor.layouts[0].stride = sizeof(Vertex); + vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; + + pipelineDescriptor.vertexDescriptor = vertexDescriptor; + + pipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; + if (!pipelineState || error) { + if (error) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create Metal render pipeline: %s", + [[error localizedDescription] UTF8String]); + } + return false; + } + + // Create vertex buffer + vertexBuffer = [device newBufferWithBytes:quadVertices + length:sizeof(quadVertices) + options:MTLResourceOptionCPUCacheModeDefault]; + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "** Metal shader system initialized successfully"); + return true; +} + +void updateMetalTexture(SDL_Texture* backBuffer) { + if (!device || !backBuffer) return; + + // Solo intentar obtener la textura una vez al inicio, después reutilizar + static bool textureInitialized = false; + if (textureInitialized && metalTexture) { + return; // Ya tenemos la textura configurada + } + + // Intentar obtener la textura Metal directamente desde SDL + SDL_PropertiesID props = SDL_GetTextureProperties(backBuffer); + if (props == 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get texture properties: %s", SDL_GetError()); + return; + } + + // Obtener la textura Metal nativa de SDL + id sdlMetalTexture = (__bridge id)SDL_GetPointerProperty(props, "SDL.texture.metal.texture", nullptr); + + if (sdlMetalTexture) { + // Usar la textura Metal directamente de SDL + metalTexture = sdlMetalTexture; + textureInitialized = true; + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Using SDL Metal texture directly (size: %lux%lu)", + [metalTexture width], [metalTexture height]); + return; + } + + // Intentar con nombres alternativos + const char* propertyNames[] = { + "texture.metal.texture", + "SDL.renderer.metal.texture", + "metal.texture" + }; + + for (const char* propName : propertyNames) { + sdlMetalTexture = (__bridge id)SDL_GetPointerProperty(props, propName, nullptr); + if (sdlMetalTexture) { + metalTexture = sdlMetalTexture; + textureInitialized = true; + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Using SDL Metal texture via property '%s'", propName); + return; + } + } + + // Si no podemos obtener la textura Metal, crear una propia que coincida con la de SDL + if (!metalTexture) { + float width_f, height_f; + if (SDL_GetTextureSize(backBuffer, &width_f, &height_f)) { + MTLTextureDescriptor* desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:(int)width_f + height:(int)height_f + mipmapped:NO]; + desc.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget; + metalTexture = [device newTextureWithDescriptor:desc]; + textureInitialized = true; + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Created fallback Metal texture (%dx%d)", + (int)width_f, (int)height_f); + } + } +} + +void renderMetal() { + if (!device || !commandQueue || !pipelineState || !metalLayer) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Metal render failed: missing components"); + return; + } + + // Get the current drawable + id drawable = [metalLayer nextDrawable]; + if (!drawable) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get Metal drawable"); + return; + } + + // Create render pass descriptor + MTLRenderPassDescriptor* renderPass = [MTLRenderPassDescriptor renderPassDescriptor]; + renderPass.colorAttachments[0].texture = drawable.texture; + renderPass.colorAttachments[0].loadAction = MTLLoadActionClear; + renderPass.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0); + renderPass.colorAttachments[0].storeAction = MTLStoreActionStore; + + // Create command buffer + id commandBuffer = [commandQueue commandBuffer]; + + // Create render command encoder + id encoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPass]; + + // Set pipeline state + [encoder setRenderPipelineState:pipelineState]; + + // Set vertex buffer + [encoder setVertexBuffer:vertexBuffer offset:0 atIndex:0]; + + // Set texture and sampler (use metalTexture if available, otherwise render solid color) + if (metalTexture) { + [encoder setFragmentTexture:metalTexture atIndex:0]; + + // Create sampler state for nearest neighbor filtering (pixelated look) + MTLSamplerDescriptor* samplerDescriptor = [[MTLSamplerDescriptor alloc] init]; + samplerDescriptor.minFilter = MTLSamplerMinMagFilterNearest; + samplerDescriptor.magFilter = MTLSamplerMinMagFilterNearest; + samplerDescriptor.sAddressMode = MTLSamplerAddressModeClampToEdge; + samplerDescriptor.tAddressMode = MTLSamplerAddressModeClampToEdge; + + id sampler = [device newSamplerStateWithDescriptor:samplerDescriptor]; + [encoder setFragmentSamplerState:sampler atIndex:0]; + } else { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "No Metal texture - rendering test pattern"); + // No texture available - the shader should handle this case + } + + // Draw the quad + [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; + + // End encoding + [encoder endEncoding]; + + // Present drawable + [commandBuffer presentDrawable:drawable]; + + // Commit command buffer + [commandBuffer commit]; +} + +void cleanupMetal() { + // Release Metal objects (ARC handles most of this automatically) + pipelineState = nullptr; + metalTexture = nullptr; + vertexBuffer = nullptr; + commandQueue = nullptr; + device = nullptr; + + if (metalView) { + SDL_Metal_DestroyView(metalView); + metalView = nullptr; + } + + metalLayer = nullptr; + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Metal shader system cleaned up"); +} + +} // namespace metal +} // namespace shader + +#endif // __APPLE__ \ No newline at end of file diff --git a/source/screen.cpp b/source/screen.cpp index 5ae077d..f30c49f 100644 --- a/source/screen.cpp +++ b/source/screen.cpp @@ -63,6 +63,7 @@ Screen::Screen() // Destructor Screen::~Screen() { + shader::cleanup(); SDL_DestroyTexture(game_canvas_); SDL_DestroyRenderer(renderer_); SDL_DestroyWindow(window_); @@ -334,7 +335,20 @@ auto Screen::initSDLVideo() -> bool { } // Crear renderer +#ifdef __APPLE__ + // Intentar crear renderer Metal específicamente en macOS + renderer_ = SDL_CreateRenderer(window_, "metal"); + if (renderer_ == nullptr) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Failed to create Metal renderer, trying default: %s", SDL_GetError()); + renderer_ = SDL_CreateRenderer(window_, nullptr); + } else { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Metal renderer created successfully"); + } +#else renderer_ = SDL_CreateRenderer(window_, nullptr); +#endif + if (renderer_ == nullptr) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "FATAL: Failed to create renderer! SDL Error: %s",