From b3f6e2fcf02f42ac1aa3bdfd3e6dda35e8da8efb Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 16 Sep 2025 07:42:03 +0200 Subject: [PATCH] Metal post-processing pipeline implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add custom Metal render target creation - Implement post-processing without GPU-CPU-GPU copies - Create Metal texture extraction system - Add Metal CRT shader pipeline integration - Modify screen rendering to use Metal when available - Enable shaders by default for testing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- source/defaults.h | 2 +- source/external/jail_shader.h | 16 +- source/external/jail_shader_metal.mm | 387 +++++++++++++++++++++++++++ source/screen.cpp | 24 ++ 4 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 source/external/jail_shader_metal.mm diff --git a/source/defaults.h b/source/defaults.h index 5b6c95d..3a2a428 100644 --- a/source/defaults.h +++ b/source/defaults.h @@ -219,7 +219,7 @@ constexpr SDL_ScaleMode VIDEO_SCALE_MODE = SDL_ScaleMode::SDL_SCALEMODE_NEAREST; constexpr bool VIDEO_FULLSCREEN = false; constexpr bool VIDEO_VSYNC = true; constexpr bool VIDEO_INTEGER_SCALE = true; -constexpr bool VIDEO_SHADERS = false; +constexpr bool VIDEO_SHADERS = true; // Music constexpr bool MUSIC_ENABLED = true; diff --git a/source/external/jail_shader.h b/source/external/jail_shader.h index 47aad0f..4abc119 100644 --- a/source/external/jail_shader.h +++ b/source/external/jail_shader.h @@ -4,6 +4,20 @@ #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); +SDL_Texture* createMetalRenderTarget(SDL_Renderer* renderer, int width, int height); +void updateMetalTexture(SDL_Texture* backBuffer); +void renderMetal(); +void renderWithPostProcessing(SDL_Renderer* renderer, SDL_Texture* sourceTexture); +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..ef843bb --- /dev/null +++ b/source/external/jail_shader_metal.mm @@ -0,0 +1,387 @@ +#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__ \ No newline at end of file diff --git a/source/screen.cpp b/source/screen.cpp index 5ae077d..c2802d6 100644 --- a/source/screen.cpp +++ b/source/screen.cpp @@ -44,8 +44,21 @@ Screen::Screen() initSDLVideo(); // Crea la textura de destino +#ifdef __APPLE__ + const auto render_name = SDL_GetRendererName(renderer_); + if (render_name && !strncmp(render_name, "metal", 5)) { + // Usar nuestra propia Metal texture como render target + game_canvas_ = shader::metal::createMetalRenderTarget(renderer_, param.game.width, param.game.height); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Using custom Metal render target for game_canvas_"); + } else { + // Fallback para otros renderers + game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height); + SDL_SetTextureScaleMode(game_canvas_, SDL_SCALEMODE_NEAREST); + } +#else game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height); SDL_SetTextureScaleMode(game_canvas_, SDL_SCALEMODE_NEAREST); +#endif // Crea el objeto de texto createText(); @@ -99,7 +112,18 @@ void Screen::renderPresent() { clean(); if (Options::video.shaders) { +#ifdef __APPLE__ + const auto render_name = SDL_GetRendererName(renderer_); + if (render_name && !strncmp(render_name, "metal", 5)) { + // Use Metal post-processing with our custom render target + shader::metal::renderWithPostProcessing(renderer_, game_canvas_); + } else { + // Fallback to standard shader system for non-Metal renderers + shader::render(); + } +#else shader::render(); +#endif } else { SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr); SDL_RenderPresent(renderer_);