#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 pipelineState = nullptr; id vertexBuffer = nullptr; id backBufferTexture = nullptr; id gameCanvasTexture = nullptr; // Our custom render target texture id sampler = nullptr; // SDL objects (references from main shader module) SDL_Window* win = nullptr; SDL_Renderer* renderer = nullptr; SDL_Texture* backBuffer = 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) - Standard OpenGL-style coordinates {{-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 ""; } SDL_Texture* createMetalRenderTarget(SDL_Renderer* renderer, int width, int height) { if (!renderer) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "createMetalRenderTarget: No renderer provided"); return nullptr; } // Crear textura Metal como render target SDL_Texture* metalTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, width, height); if (!metalTexture) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create Metal render target texture: %s", SDL_GetError()); return nullptr; } // Configurar filtrado nearest neighbor para look pixelado SDL_SetTextureScaleMode(metalTexture, SDL_SCALEMODE_NEAREST); // Try to extract and store the Metal texture directly SDL_PropertiesID props = SDL_GetTextureProperties(metalTexture); if (props != 0) { const char* propertyNames[] = { "SDL.texture.metal.texture", "SDL.renderer.metal.texture", "metal.texture", "texture.metal", "MTLTexture", "texture" }; for (const char* propName : propertyNames) { gameCanvasTexture = (__bridge id)SDL_GetPointerProperty(props, propName, nullptr); if (gameCanvasTexture) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Successfully extracted Metal texture via property '%s' (size: %lux%lu)", propName, [gameCanvasTexture width], [gameCanvasTexture height]); break; } } } if (!gameCanvasTexture) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Could not extract Metal texture from SDL texture - shaders may not work"); } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Created Metal render target texture (%dx%d)", width, height); return metalTexture; } bool initMetal(SDL_Window* window, SDL_Texture* backBufferTexture, const std::string& shaderFilename) { // Store references win = window; backBuffer = backBufferTexture; renderer = SDL_GetRenderer(window); if (!renderer) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get SDL renderer"); return false; } // Get Metal layer from SDL renderer and extract device from it CAMetalLayer* metalLayer = (__bridge CAMetalLayer*)SDL_GetRenderMetalLayer(renderer); if (!metalLayer) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get Metal layer from SDL renderer"); return false; } device = metalLayer.device; if (!device) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get Metal device from layer"); return false; } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got Metal device from SDL layer: %s", [[device name] UTF8String]); // Note: We no longer need our own texture - we'll use the backBuffer directly // 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 = MTLPixelFormatBGRA8Unorm; // 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]; // 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; sampler = [device newSamplerStateWithDescriptor:samplerDescriptor]; SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "** Metal shader system initialized successfully"); return true; } void updateMetalTexture(SDL_Texture* backBuffer) { if (!device || !backBuffer) return; // Only log this occasionally to avoid spam static int attemptCount = 0; static bool hasLogged = false; // Try multiple property names that SDL3 might use SDL_PropertiesID props = SDL_GetTextureProperties(backBuffer); if (props != 0) { const char* propertyNames[] = { "SDL.texture.metal.texture", "SDL.renderer.metal.texture", "metal.texture", "texture.metal", "MTLTexture", "texture" }; for (const char* propName : propertyNames) { id sdlMetalTexture = (__bridge id)SDL_GetPointerProperty(props, propName, nullptr); if (sdlMetalTexture) { backBufferTexture = sdlMetalTexture; if (!hasLogged) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got Metal texture via property '%s' (size: %lux%lu)", propName, [backBufferTexture width], [backBufferTexture height]); hasLogged = true; } return; } } } // If we can't get the texture after several attempts, log once and continue if (!hasLogged && attemptCount++ > 10) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Could not access SDL Metal texture after %d attempts - shader will be skipped", attemptCount); hasLogged = true; } } void renderMetal() { if (!renderer || !device || !pipelineState) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Metal render failed: missing components"); return; } // Correct pipeline: backBuffer → Metal shaders → screen (no double rendering) // Try to get the Metal texture directly from the SDL backBuffer texture updateMetalTexture(backBuffer); if (!backBufferTexture) { static int fallbackLogCount = 0; if (fallbackLogCount++ < 3) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Metal texture not available - falling back to normal SDL rendering (attempt %d)", fallbackLogCount); } // Fallback: render without shaders using normal SDL path SDL_SetRenderTarget(renderer, nullptr); SDL_RenderTexture(renderer, backBuffer, nullptr, nullptr); SDL_RenderPresent(renderer); return; } // Apply Metal CRT shader directly: backBuffer texture → screen SDL_SetRenderTarget(renderer, nullptr); // Get Metal command encoder to render directly to screen void* encoder_ptr = SDL_GetRenderMetalCommandEncoder(renderer); if (encoder_ptr) { id encoder = (__bridge id)encoder_ptr; static int debugCount = 0; if (debugCount++ < 5) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Metal render attempt %d: encoder=%p, pipeline=%p, texture=%p", debugCount, encoder, pipelineState, backBufferTexture); } // Apply CRT shader effect directly to backBuffer texture [encoder setRenderPipelineState:pipelineState]; [encoder setVertexBuffer:vertexBuffer offset:0 atIndex:0]; [encoder setFragmentTexture:backBufferTexture atIndex:0]; [encoder setFragmentSamplerState:sampler atIndex:0]; // Draw fullscreen quad with CRT effect directly to screen [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; static int successCount = 0; if (successCount++ < 5) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Applied CRT shader to backBuffer (attempt %d) - texture size: %lux%lu", successCount, [backBufferTexture width], [backBufferTexture height]); } } else { // Fallback: render normally without shaders SDL_RenderTexture(renderer, backBuffer, nullptr, nullptr); static int fallbackCount = 0; if (fallbackCount++ < 3) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to get Metal command encoder - fallback rendering"); } } SDL_RenderPresent(renderer); } void renderWithPostProcessing(SDL_Renderer* renderer, SDL_Texture* sourceTexture) { if (!renderer || !sourceTexture || !device || !pipelineState) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Metal post-processing failed: missing components"); // Fallback: render normally without shaders SDL_SetRenderTarget(renderer, nullptr); SDL_RenderTexture(renderer, sourceTexture, nullptr, nullptr); SDL_RenderPresent(renderer); return; } // Use our stored Metal texture if available id metalTexture = gameCanvasTexture; if (!metalTexture) { static int fallbackLogCount = 0; if (fallbackLogCount++ < 3) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "gameCanvasTexture not available - falling back to normal rendering (attempt %d)", fallbackLogCount); } // Fallback: render normally without shaders SDL_SetRenderTarget(renderer, nullptr); SDL_RenderTexture(renderer, sourceTexture, nullptr, nullptr); SDL_RenderPresent(renderer); return; } // Apply Metal CRT shader post-processing: sourceTexture → screen SDL_SetRenderTarget(renderer, nullptr); // Get Metal command encoder to render directly to screen void* encoder_ptr = SDL_GetRenderMetalCommandEncoder(renderer); if (encoder_ptr) { id encoder = (__bridge id)encoder_ptr; static int debugCount = 0; if (debugCount++ < 3) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Metal post-processing attempt %d: encoder=%p, pipeline=%p, texture=%p", debugCount, encoder, pipelineState, metalTexture); } // Apply CRT shader effect to sourceTexture [encoder setRenderPipelineState:pipelineState]; [encoder setVertexBuffer:vertexBuffer offset:0 atIndex:0]; [encoder setFragmentTexture:metalTexture atIndex:0]; [encoder setFragmentSamplerState:sampler atIndex:0]; // Draw fullscreen quad with CRT effect directly to screen [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; static int successCount = 0; if (successCount++ < 3) { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Applied CRT post-processing (attempt %d) - texture size: %lux%lu", successCount, [metalTexture width], [metalTexture height]); } } else { // Fallback: render normally without shaders SDL_RenderTexture(renderer, sourceTexture, nullptr, nullptr); static int fallbackCount = 0; if (fallbackCount++ < 3) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to get Metal command encoder for post-processing - fallback rendering"); } } SDL_RenderPresent(renderer); } void cleanupMetal() { // Release Metal objects (ARC handles most of this automatically) pipelineState = nullptr; backBufferTexture = nullptr; gameCanvasTexture = nullptr; vertexBuffer = nullptr; sampler = nullptr; device = nullptr; renderer = nullptr; SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Metal shader system cleaned up"); } } // namespace metal } // namespace shader #endif // __APPLE__