Files
vibe5_metal/source/main.cpp
Sergio Valor 7cccedb5fb Extraer shaders a archivos .metal separados para mejor organización
- Crear directorio data/shaders/ para organizar todos los shaders MSL
- Extraer shaders embebidos a archivos individuales:
  * background.metal - Shader de fondo degradado
  * triangle.metal - Shader de triángulo multicolor
  * sprite.metal - Shader de sprites con vertex color tinting
  * crt.metal - Shader CRT post-processing completo
- Modificar main.cpp para cargar shaders desde archivos:
  * Usar stringWithContentsOfFile para leer código fuente
  * Compilar dinámicamente con newLibraryWithSource
  * Manejo robusto de errores de lectura y compilación
- Eliminar 351 líneas de strings embebidos de main.cpp
- Mantener funcionalidad completa: CRT + sprites + fondo + triángulo

Beneficios:
- Shaders editables sin recompilar ejecutable
- Mejor organización y mantenimiento del código
- Syntax highlighting completo en editores
- Reutilización de shaders en otros proyectos
- Desarrollo más ágil de efectos visuales

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-28 18:40:28 +02:00

517 lines
23 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <iostream>
#include <SDL3/SDL.h>
// Headers Metal usando Objective-C++
#ifdef __APPLE__
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#endif
/*
* METAL PERFORMANCE HUD - Debug FPS y métricas en tiempo real
*
* Para mostrar FPS y estadísticas de rendimiento, ejecutar con:
*
* MTL_HUD_ENABLED=1 ./vibe5_metal
*
* El HUD aparece en la esquina superior derecha y muestra:
* - FPS (Frames Per Second)
* - CPU y GPU usage
* - Memory usage
* - Device info y resolución
*
* Atajos de teclado disponibles cuando el HUD está activo:
* - Command+F9: Enable/Disable HUD
* - Command+F8: Enable/Disable Logging
* - Command+F11: Reset Metrics
* - Command+F12: Show Configuration Panel
*
* Para activar globalmente para todas las apps Metal:
* launchctl setenv MTL_HUD_ENABLED 1
*
* Para desactivar:
* launchctl setenv MTL_HUD_ENABLED 0
*/
// Estructura para vértices de sprites
struct SpriteVertex {
float position[2]; // x, y coordinates
float color[4]; // r, g, b, a color components
float texCoord[2]; // u, v texture coordinates
};
int main(int argc, char* argv[]) {
#ifdef __APPLE__
// Configurar SDL para usar Metal
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal");
// Inicializar SDL
if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cout << "SDL_Init failed: " << SDL_GetError() << std::endl;
return -1;
}
// Crear ventana
SDL_Window* window = SDL_CreateWindow("vibe5_metal - Triangle + Gradient + Sprites", 960, 720, SDL_WINDOW_HIGH_PIXEL_DENSITY);
if (!window) {
std::cout << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl;
SDL_Quit();
return -1;
}
// Crear renderer
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
if (!renderer) {
std::cout << "SDL_CreateRenderer failed: " << SDL_GetError() << std::endl;
SDL_DestroyWindow(window);
SDL_Quit();
return -1;
}
std::cout << "SDL Window and Renderer created successfully" << std::endl;
// Obtener Metal layer
CAMetalLayer* metalLayer = (__bridge CAMetalLayer*)SDL_GetRenderMetalLayer(renderer);
if (!metalLayer) {
std::cout << "Failed to get Metal layer - SDL may not be using Metal driver" << std::endl;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return -1;
}
// Obtener device Metal del layer
id<MTLDevice> device = metalLayer.device;
if (!device) {
std::cout << "Failed to get Metal device from layer" << std::endl;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return -1;
}
std::cout << "Got Metal device: " << [[device name] UTF8String] << std::endl;
// Crear command queue
id<MTLCommandQueue> commandQueue = [device newCommandQueue];
if (!commandQueue) {
std::cout << "Failed to create Metal command queue" << std::endl;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return -1;
}
// Cargar shaders CRT desde archivo
NSError* error = nil;
NSString* crtShaderPath = @"data/shaders/crt.metal";
NSString* crtShaderSource = [NSString stringWithContentsOfFile:crtShaderPath encoding:NSUTF8StringEncoding error:&error];
if (!crtShaderSource || error) {
if (error) {
std::cout << "Failed to read CRT shader file: " << [[error localizedDescription] UTF8String] << std::endl;
} else {
std::cout << "Failed to read CRT shader file: data/shaders/crt.metal" << std::endl;
}
return -1;
}
id<MTLLibrary> crtLibrary = [device newLibraryWithSource:crtShaderSource options:nil error:&error];
if (!crtLibrary || error) {
if (error) {
std::cout << "Failed to compile CRT shader: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
id<MTLFunction> crtVertexFunction = [crtLibrary newFunctionWithName:@"crt_vertex_main"];
id<MTLFunction> crtFragmentFunction = [crtLibrary newFunctionWithName:@"crt_fragment_main"];
// Cargar shaders de fondo desde archivo
NSString* backgroundShaderPath = @"data/shaders/background.metal";
NSString* backgroundShaderSource = [NSString stringWithContentsOfFile:backgroundShaderPath encoding:NSUTF8StringEncoding error:&error];
if (!backgroundShaderSource || error) {
if (error) {
std::cout << "Failed to read background shader file: " << [[error localizedDescription] UTF8String] << std::endl;
} else {
std::cout << "Failed to read background shader file: data/shaders/background.metal" << std::endl;
}
return -1;
}
id<MTLLibrary> backgroundLibrary = [device newLibraryWithSource:backgroundShaderSource options:nil error:&error];
if (!backgroundLibrary || error) {
if (error) {
std::cout << "Failed to compile background shader: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
id<MTLFunction> backgroundVertexFunction = [backgroundLibrary newFunctionWithName:@"background_vertex_main"];
id<MTLFunction> backgroundFragmentFunction = [backgroundLibrary newFunctionWithName:@"background_fragment_main"];
// Cargar shaders de triángulo desde archivo
NSString* triangleShaderPath = @"data/shaders/triangle.metal";
NSString* triangleShaderSource = [NSString stringWithContentsOfFile:triangleShaderPath encoding:NSUTF8StringEncoding error:&error];
if (!triangleShaderSource || error) {
if (error) {
std::cout << "Failed to read triangle shader file: " << [[error localizedDescription] UTF8String] << std::endl;
} else {
std::cout << "Failed to read triangle shader file: data/shaders/triangle.metal" << std::endl;
}
return -1;
}
id<MTLLibrary> triangleLibrary = [device newLibraryWithSource:triangleShaderSource options:nil error:&error];
if (!triangleLibrary || error) {
if (error) {
std::cout << "Failed to compile triangle shader: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
id<MTLFunction> triangleVertexFunction = [triangleLibrary newFunctionWithName:@"triangle_vertex_main"];
id<MTLFunction> triangleFragmentFunction = [triangleLibrary newFunctionWithName:@"triangle_fragment_main"];
// Cargar shaders de sprites desde archivo
NSString* spriteShaderPath = @"data/shaders/sprite.metal";
NSString* spriteShaderSource = [NSString stringWithContentsOfFile:spriteShaderPath encoding:NSUTF8StringEncoding error:&error];
if (!spriteShaderSource || error) {
if (error) {
std::cout << "Failed to read sprite shader file: " << [[error localizedDescription] UTF8String] << std::endl;
} else {
std::cout << "Failed to read sprite shader file: data/shaders/sprite.metal" << std::endl;
}
return -1;
}
id<MTLLibrary> spriteLibrary = [device newLibraryWithSource:spriteShaderSource options:nil error:&error];
if (!spriteLibrary || error) {
if (error) {
std::cout << "Failed to compile sprite shader: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
id<MTLFunction> spriteVertexFunction = [spriteLibrary newFunctionWithName:@"sprite_vertex_main"];
id<MTLFunction> spriteFragmentFunction = [spriteLibrary newFunctionWithName:@"sprite_fragment_main"];
// Crear pipeline de fondo
MTLRenderPipelineDescriptor* backgroundPipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
backgroundPipelineDescriptor.vertexFunction = backgroundVertexFunction;
backgroundPipelineDescriptor.fragmentFunction = backgroundFragmentFunction;
backgroundPipelineDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat;
id<MTLRenderPipelineState> backgroundPipelineState = [device newRenderPipelineStateWithDescriptor:backgroundPipelineDescriptor error:&error];
if (!backgroundPipelineState || error) {
if (error) {
std::cout << "Failed to create background render pipeline: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
// Crear pipeline de triángulo
MTLRenderPipelineDescriptor* trianglePipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
trianglePipelineDescriptor.vertexFunction = triangleVertexFunction;
trianglePipelineDescriptor.fragmentFunction = triangleFragmentFunction;
trianglePipelineDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat;
id<MTLRenderPipelineState> trianglePipelineState = [device newRenderPipelineStateWithDescriptor:trianglePipelineDescriptor error:&error];
if (!trianglePipelineState || error) {
if (error) {
std::cout << "Failed to create triangle render pipeline: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
// Crear pipeline de sprites con alpha blending
MTLRenderPipelineDescriptor* spritePipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
spritePipelineDescriptor.vertexFunction = spriteVertexFunction;
spritePipelineDescriptor.fragmentFunction = spriteFragmentFunction;
spritePipelineDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat;
// Configurar alpha blending
spritePipelineDescriptor.colorAttachments[0].blendingEnabled = YES;
spritePipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
spritePipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
spritePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
spritePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorSourceAlpha;
spritePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
spritePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
// Configurar vertex descriptor para sprites (position + color + texCoord)
MTLVertexDescriptor* spriteVertexDescriptor = [[MTLVertexDescriptor alloc] init];
spriteVertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position
spriteVertexDescriptor.attributes[0].offset = 0;
spriteVertexDescriptor.attributes[0].bufferIndex = 0;
spriteVertexDescriptor.attributes[1].format = MTLVertexFormatFloat4; // color
spriteVertexDescriptor.attributes[1].offset = 8;
spriteVertexDescriptor.attributes[1].bufferIndex = 0;
spriteVertexDescriptor.attributes[2].format = MTLVertexFormatFloat2; // texCoord
spriteVertexDescriptor.attributes[2].offset = 24;
spriteVertexDescriptor.attributes[2].bufferIndex = 0;
spriteVertexDescriptor.layouts[0].stride = sizeof(SpriteVertex);
spriteVertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
spritePipelineDescriptor.vertexDescriptor = spriteVertexDescriptor;
id<MTLRenderPipelineState> spritePipelineState = [device newRenderPipelineStateWithDescriptor:spritePipelineDescriptor error:&error];
if (!spritePipelineState || error) {
if (error) {
std::cout << "Failed to create sprite render pipeline: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
// Cargar textura de sprite
NSString* texturePath = @"data/ball.png";
NSImage* image = [[NSImage alloc] initWithContentsOfFile:texturePath];
if (!image) {
std::cout << "Failed to load texture image: data/ball.png" << std::endl;
return -1;
}
// Obtener representación como bitmap
NSBitmapImageRep* bitmap = [[NSBitmapImageRep alloc] initWithData:[image TIFFRepresentation]];
if (!bitmap) {
std::cout << "Failed to create bitmap from image" << std::endl;
return -1;
}
// Crear descriptor de textura Metal
MTLTextureDescriptor* textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
width:[bitmap pixelsWide]
height:[bitmap pixelsHigh]
mipmapped:NO];
// Crear textura Metal
id<MTLTexture> spriteTexture = [device newTextureWithDescriptor:textureDescriptor];
if (!spriteTexture) {
std::cout << "Failed to create Metal texture" << std::endl;
return -1;
}
// Copiar datos de imagen a textura Metal
MTLRegion region = MTLRegionMake2D(0, 0, [bitmap pixelsWide], [bitmap pixelsHigh]);
[spriteTexture replaceRegion:region
mipmapLevel:0
withBytes:[bitmap bitmapData]
bytesPerRow:[bitmap bytesPerRow]];
// Crear sampler con filtro NEAREST para evitar blur
MTLSamplerDescriptor* samplerDescriptor = [[MTLSamplerDescriptor alloc] init];
samplerDescriptor.minFilter = MTLSamplerMinMagFilterNearest;
samplerDescriptor.magFilter = MTLSamplerMinMagFilterNearest;
samplerDescriptor.sAddressMode = MTLSamplerAddressModeClampToEdge;
samplerDescriptor.tAddressMode = MTLSamplerAddressModeClampToEdge;
id<MTLSamplerState> textureSampler = [device newSamplerStateWithDescriptor:samplerDescriptor];
if (!textureSampler) {
std::cout << "Failed to create Metal sampler" << std::endl;
return -1;
}
// Crear vertex buffer para sprites (para 5 sprites = 30 vértices)
id<MTLBuffer> spriteVertexBuffer = [device newBufferWithLength:4096 options:MTLResourceCPUCacheModeDefaultCache];
if (!spriteVertexBuffer) {
std::cout << "Failed to create sprite vertex buffer" << std::endl;
return -1;
}
// Crear offscreen render target para CRT post-processing
CGSize drawableSize = metalLayer.drawableSize;
MTLTextureDescriptor* offscreenTextureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
width:(NSUInteger)drawableSize.width
height:(NSUInteger)drawableSize.height
mipmapped:NO];
offscreenTextureDescriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
id<MTLTexture> offscreenTexture = [device newTextureWithDescriptor:offscreenTextureDescriptor];
if (!offscreenTexture) {
std::cout << "Failed to create offscreen render target" << std::endl;
return -1;
}
// Crear pipeline CRT post-processing
MTLRenderPipelineDescriptor* crtPipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
crtPipelineDescriptor.vertexFunction = crtVertexFunction;
crtPipelineDescriptor.fragmentFunction = crtFragmentFunction;
crtPipelineDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat;
id<MTLRenderPipelineState> crtPipelineState = [device newRenderPipelineStateWithDescriptor:crtPipelineDescriptor error:&error];
if (!crtPipelineState || error) {
if (error) {
std::cout << "Failed to create CRT render pipeline: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
// Crear sampler para CRT (linear para suavizar la escalada)
MTLSamplerDescriptor* crtSamplerDescriptor = [[MTLSamplerDescriptor alloc] init];
crtSamplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear;
crtSamplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear;
crtSamplerDescriptor.sAddressMode = MTLSamplerAddressModeClampToEdge;
crtSamplerDescriptor.tAddressMode = MTLSamplerAddressModeClampToEdge;
id<MTLSamplerState> crtSampler = [device newSamplerStateWithDescriptor:crtSamplerDescriptor];
if (!crtSampler) {
std::cout << "Failed to create CRT sampler" << std::endl;
return -1;
}
std::cout << "Metal pipelines created successfully (background + triangle + sprites + CRT)" << std::endl;
std::cout << "Sprite texture loaded: " << [bitmap pixelsWide] << "x" << [bitmap pixelsHigh] << std::endl;
std::cout << "Offscreen render target created: " << (int)drawableSize.width << "x" << (int)drawableSize.height << std::endl;
// Main loop
bool quit = false;
SDL_Event e;
while (!quit) {
while (SDL_PollEvent(&e)) {
if (e.type == SDL_EVENT_QUIT) {
quit = true;
}
}
// Obtener drawable del Metal layer
id<CAMetalDrawable> drawable = [metalLayer nextDrawable];
if (!drawable) {
continue;
}
// PASO 1: Renderizar escena a offscreen texture
MTLRenderPassDescriptor* offscreenRenderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
offscreenRenderPassDescriptor.colorAttachments[0].texture = offscreenTexture;
offscreenRenderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
offscreenRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0);
offscreenRenderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
// Crear command buffer
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
if (!commandBuffer) {
continue;
}
// Crear render command encoder para offscreen
id<MTLRenderCommandEncoder> offscreenEncoder = [commandBuffer renderCommandEncoderWithDescriptor:offscreenRenderPassDescriptor];
if (!offscreenEncoder) {
continue;
}
// 1. Dibujar fondo degradado primero
[offscreenEncoder setRenderPipelineState:backgroundPipelineState];
[offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
// 2. Dibujar triángulo encima
[offscreenEncoder setRenderPipelineState:trianglePipelineState];
[offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
// 3. Dibujar 5 sprites con diferentes colores
[offscreenEncoder setRenderPipelineState:spritePipelineState];
// Configurar textura y sampler para sprites
[offscreenEncoder setFragmentTexture:spriteTexture atIndex:0];
[offscreenEncoder setFragmentSamplerState:textureSampler atIndex:0];
// Crear 5 sprites en diferentes posiciones con diferentes colores
SpriteVertex spriteVertices[30]; // 5 sprites × 6 vértices cada uno
// Configuración común
float spriteSize = 30.0f;
float windowWidth = 960.0f;
float windowHeight = 720.0f;
float halfWidth = (spriteSize / windowWidth);
float halfHeight = (spriteSize / windowHeight);
// Posiciones de los 5 sprites (evitando el centro donde está el triángulo)
float positions[5][2] = {
{-0.6f, 0.6f}, // Esquina superior izquierda
{ 0.6f, 0.6f}, // Esquina superior derecha
{-0.6f, -0.6f}, // Esquina inferior izquierda
{ 0.6f, -0.6f}, // Esquina inferior derecha
{ 0.0f, -0.8f} // Centro inferior
};
// Colores para cada sprite (RGBA)
float colors[5][4] = {
{1.0f, 0.0f, 0.0f, 1.0f}, // Rojo
{0.0f, 1.0f, 0.0f, 1.0f}, // Verde
{0.0f, 0.0f, 1.0f, 1.0f}, // Azul
{1.0f, 1.0f, 0.0f, 1.0f}, // Amarillo
{1.0f, 0.0f, 1.0f, 1.0f} // Magenta
};
// Generar vértices para los 5 sprites
for (int i = 0; i < 5; i++) {
float centerX = positions[i][0];
float centerY = positions[i][1];
// Índice base para este sprite
int baseIndex = i * 6;
// Primer triángulo (bottom-left, bottom-right, top-left)
spriteVertices[baseIndex + 0] = {{centerX - halfWidth, centerY - halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {0.0f, 1.0f}};
spriteVertices[baseIndex + 1] = {{centerX + halfWidth, centerY - halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {1.0f, 1.0f}};
spriteVertices[baseIndex + 2] = {{centerX - halfWidth, centerY + halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {0.0f, 0.0f}};
// Segundo triángulo (bottom-right, top-right, top-left)
spriteVertices[baseIndex + 3] = {{centerX + halfWidth, centerY - halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {1.0f, 1.0f}};
spriteVertices[baseIndex + 4] = {{centerX + halfWidth, centerY + halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {1.0f, 0.0f}};
spriteVertices[baseIndex + 5] = {{centerX - halfWidth, centerY + halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {0.0f, 0.0f}};
}
// Copiar vértices al buffer
void* spriteData = [spriteVertexBuffer contents];
memcpy(spriteData, spriteVertices, sizeof(spriteVertices));
// Configurar vertex buffer y dibujar todos los sprites en offscreen
[offscreenEncoder setVertexBuffer:spriteVertexBuffer offset:0 atIndex:0];
[offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:30];
[offscreenEncoder endEncoding];
// PASO 2: Aplicar CRT post-processing a la pantalla final
MTLRenderPassDescriptor* finalRenderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
finalRenderPassDescriptor.colorAttachments[0].texture = drawable.texture;
finalRenderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
finalRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0);
finalRenderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
// Crear encoder para CRT post-processing
id<MTLRenderCommandEncoder> crtEncoder = [commandBuffer renderCommandEncoderWithDescriptor:finalRenderPassDescriptor];
if (!crtEncoder) {
continue;
}
// Aplicar CRT shader al resultado offscreen
[crtEncoder setRenderPipelineState:crtPipelineState];
[crtEncoder setFragmentTexture:offscreenTexture atIndex:0];
[crtEncoder setFragmentSamplerState:crtSampler atIndex:0];
[crtEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
[crtEncoder endEncoding];
// Presentar drawable
[commandBuffer presentDrawable:drawable];
[commandBuffer commit];
}
// Cleanup
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
std::cout << "vibe5_metal finished successfully" << std::endl;
return 0;
#else
std::cout << "This example requires macOS and Metal support" << std::endl;
return -1;
#endif
}