//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#include <MaterialXView/RenderPipelineMetal.h>
#include <MaterialXView/Viewer.h>
#include <MaterialXRenderMsl/MetalTextureHandler.h>

#include <MaterialXGenMsl/MslShaderGenerator.h>
#include <MaterialXRenderMsl/MetalState.h>
#include <MaterialXRenderMsl/TextureBaker.h>
#include <MaterialXRenderMsl/MetalFramebuffer.h>
#include <MaterialXRenderMsl/MslMaterial.h>

#include <nanogui/messagedialog.h>

#include <MetalPerformanceShaders/MetalPerformanceShaders.h>

namespace
{

const float PI = std::acos(-1.0f);

}

MetalRenderPipeline::MetalRenderPipeline(Viewer* viewerPtr) :
    RenderPipeline(viewerPtr)
{
}

void MetalRenderPipeline::initialize(void* metal_device, void* metal_cmd_queue)
{
    MTL(initialize((id<MTLDevice>)metal_device,
                   (id<MTLCommandQueue>)metal_cmd_queue));
}

mx::ImageHandlerPtr MetalRenderPipeline::createImageHandler()
{
    return mx::MetalTextureHandler::create(
        MTL(device),
        mx::StbImageLoader::create());
}

mx::MaterialPtr MetalRenderPipeline::createMaterial()
{
    return mx::MslMaterial::create();
}

std::shared_ptr<void> MetalRenderPipeline::createTextureBaker(unsigned int width,
                                                              unsigned int height,
                                                              mx::Image::BaseType baseType)
{
    return std::static_pointer_cast<void>(mx::TextureBakerMsl::create(width, height, baseType));
}

void MetalRenderPipeline::initFramebuffer(int width, int height,
                                          void* color_texture)
{
    MTL_PUSH_FRAMEBUFFER(mx::MetalFramebuffer::create(
                            MTL(device),
                            width * _viewer->m_pixel_ratio,
                            height * _viewer->m_pixel_ratio,
                            4, mx::Image::BaseType::HALF,
                            MTL(supportsTiledPipeline) ?
                            (id<MTLTexture>)color_texture : nil,
                            false, MTLPixelFormatRGBA16Float));
}

void MetalRenderPipeline::resizeFramebuffer(int width, int height,
                                            void* color_texture)
{
    MTL_POP_FRAMEBUFFER();
    initFramebuffer(width, height, color_texture);
}

void MetalRenderPipeline::updateAlbedoTable(int tableSize)
{
    auto& genContext    = _viewer->_genContext;
    auto& lightHandler  = _viewer->_lightHandler;
    auto& imageHandler  = _viewer->_imageHandler;
    
    if (lightHandler->getAlbedoTable())
    {
        return;
    }
    
    // Create framebuffer.
    mx::MetalFramebufferPtr framebuffer = mx::MetalFramebuffer::create(MTL(device),
                            tableSize, tableSize,
                            2,
                            mx::Image::BaseType::FLOAT);
        
        bool captureCommandBuffer = false;
        if(captureCommandBuffer)
            MTL_TRIGGER_CAPTURE;
    
        MTL_PUSH_FRAMEBUFFER(framebuffer);
        
        MTL(beginCommandBuffer());
        
        MTLRenderPassDescriptor* renderpassDesc = [MTLRenderPassDescriptor new];
        
        [renderpassDesc.colorAttachments[0] setTexture:framebuffer->getColorTexture()];
        [renderpassDesc.colorAttachments[0] setClearColor:MTLClearColorMake(0.0f, 0.0f, 0.0f, 0.0f)];
        [renderpassDesc.colorAttachments[0] setLoadAction:MTLLoadActionClear];
        [renderpassDesc.colorAttachments[0] setStoreAction:MTLStoreActionStore];
        
        [renderpassDesc.depthAttachment setTexture:framebuffer->getDepthTexture()];
        [renderpassDesc.depthAttachment setClearDepth:1.0];
        [renderpassDesc.depthAttachment setLoadAction:MTLLoadActionClear];
        [renderpassDesc.depthAttachment setStoreAction:MTLStoreActionStore];
        [renderpassDesc setStencilAttachment:nil];
        
        MTL(beginEncoder(renderpassDesc));

    // Create shader.
    mx::ShaderPtr hwShader = mx::createAlbedoTableShader(genContext, _viewer->_stdLib, "__ALBEDO_TABLE_SHADER__");
    mx::MslMaterialPtr material = mx::MslMaterial::create();
    try
    {
        material->generateShader(hwShader);
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(_viewer, ng::MessageDialog::Type::Warning, "Failed to generate albedo table shader", e.what());
        return;
    }

    // Render albedo table.
    material->bindShader();
    if (material->getProgram()->hasUniform(mx::HW::ALBEDO_TABLE_SIZE))
    {
        material->getProgram()->bindUniform(mx::HW::ALBEDO_TABLE_SIZE, mx::Value::createValue(tableSize));
    }
    material->getProgram()->prepareUsedResources(
                    MTL(renderCmdEncoder),
                    _viewer->_identityCamera,
                    nullptr,
                    imageHandler,
                    lightHandler);
    _viewer->renderScreenSpaceQuad(material);
    
    MTL(endCommandBuffer());
    
    MTL_POP_FRAMEBUFFER();
    
    if(captureCommandBuffer)
        MTL_STOP_CAPTURE;

    // Store albedo table image.
    imageHandler->releaseRenderResources(lightHandler->getAlbedoTable());
    lightHandler->setAlbedoTable(framebuffer->getColorImage(MTL(cmdQueue)));
    if (_viewer->_saveGeneratedLights)
    {
        imageHandler->saveImage("AlbedoTable.exr", lightHandler->getAlbedoTable());
    }
}

void MetalRenderPipeline::updatePrefilteredMap()
{
    auto& genContext    = _viewer->_genContext;
    auto& lightHandler  = _viewer->_lightHandler;
    auto& imageHandler  = _viewer->_imageHandler;

    if (lightHandler->getEnvPrefilteredMap())
    {
        return;
    }

    mx::ImagePtr srcTex = lightHandler->getEnvRadianceMap();
    int w = srcTex->getWidth();
    int h = srcTex->getHeight();

    mx::MetalTextureHandlerPtr mtlImageHandler = std::dynamic_pointer_cast<mx::MetalTextureHandler>(imageHandler);
    mx::ImagePtr outTex = mx::Image::create(w, h, 3, mx::Image::BaseType::HALF);
    mtlImageHandler->createRenderResources(outTex, true, true);
    id<MTLTexture> metalTex = mtlImageHandler->getAssociatedMetalTexture(outTex);

    // Create framebuffer.
    _prefilterFramebuffer = mx::MetalFramebuffer::create(
        MTL(device),
        w, h,
        4,
        mx::Image::BaseType::HALF,
        metalTex
    );

    MTL_PUSH_FRAMEBUFFER(_prefilterFramebuffer);

    // Create shader.
    mx::ShaderPtr hwShader = mx::createEnvPrefilterShader(genContext, _viewer->_stdLib, "__ENV_PREFILTER__");
    mx::MslMaterialPtr material = mx::MslMaterial::create();
    try
    {
        material->generateShader(hwShader);
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(_viewer, ng::MessageDialog::Type::Warning, "Failed to generate convolution shader", e.what());
    }

    int i = 0;

    while (w > 0 && h > 0)
    {
        MTL(beginCommandBuffer());
        MTLRenderPassDescriptor* desc = [MTLRenderPassDescriptor new];
        [desc.colorAttachments[0] setTexture:metalTex];
        [desc.colorAttachments[0] setLevel:i];
        [desc.colorAttachments[0] setLoadAction:MTLLoadActionDontCare];
        [desc.colorAttachments[0] setStoreAction:MTLStoreActionStore];
        [desc.depthAttachment setTexture:_prefilterFramebuffer->getDepthTexture()];
        [desc.depthAttachment setLoadAction:MTLLoadActionDontCare];
        [desc.depthAttachment setStoreAction:MTLStoreActionDontCare];
        [desc setStencilAttachment:nil];
        
        MTL(beginEncoder(desc));
        [MTL(renderCmdEncoder) setDepthStencilState:MTL_DEPTHSTENCIL_STATE(opaque)];

        _prefilterFramebuffer->bind(desc);
        material->bindShader();
        material->getProgram()->bindUniform(mx::HW::ENV_PREFILTER_MIP, mx::Value::createValue(i));

        bool prevValue = lightHandler->getUsePrefilteredMap();
        lightHandler->setUsePrefilteredMap(false);
        material->getProgram()->prepareUsedResources(
                        MTL(renderCmdEncoder),
                        _viewer->_identityCamera,
                        nullptr,
                        imageHandler,
                        lightHandler);
        lightHandler->setUsePrefilteredMap(prevValue);

        _viewer->renderScreenSpaceQuad(material);

        MTL(endCommandBuffer());
        [desc release];

        w /= 2;
        h /= 2;
        i++;
    }

    MTL_POP_FRAMEBUFFER();

    lightHandler->setEnvPrefilteredMap(outTex);
}

mx::ImagePtr MetalRenderPipeline::getShadowMap(int shadowMapSize)
{
    auto& genContext      = _viewer->_genContext;
    auto& lightHandler    = _viewer->_lightHandler;
    auto& imageHandler    = _viewer->_imageHandler;
    auto& shadowCamera    = _viewer->_shadowCamera;
    auto& stdLib          = _viewer->_stdLib;
    auto& geometryHandler = _viewer->_geometryHandler;
    auto& identityCamera  = _viewer->_identityCamera;
    
    mx::MetalTextureHandlerPtr mtlImageHandler =
        std::dynamic_pointer_cast<mx::MetalTextureHandler>(imageHandler);
    
    id<MTLTexture> shadowMapTex[SHADOWMAP_TEX_COUNT];
    for(int i = 0; i < SHADOWMAP_TEX_COUNT; ++i)
    {
        if(!_shadowMap[i] || _shadowMap[i]->getWidth() != shadowMapSize ||
           !mtlImageHandler->getAssociatedMetalTexture(_shadowMap[i]))
        {
            _shadowMap[i] = mx::Image::create(shadowMapSize, shadowMapSize, 2, mx::Image::BaseType::FLOAT);
            _viewer->_imageHandler->createRenderResources(_shadowMap[i], false, true);
        }
        
        shadowMapTex[i] =
            mtlImageHandler->getAssociatedMetalTexture(_shadowMap[i]);
    }
    
    if (!_viewer->_shadowMap)
    {
        // Create framebuffer.
        if(!_shadowMapFramebuffer)
        {
            _shadowMapFramebuffer = mx::MetalFramebuffer::create(
                                                       MTL(device),
                                                       shadowMapSize,
                                                       shadowMapSize,
                                                       2,
                                                       mx::Image::BaseType::FLOAT,
                                                       shadowMapTex[0]);
        }
        MTL_PUSH_FRAMEBUFFER(_shadowMapFramebuffer);
        
        // Generate shaders for shadow rendering.
        if (!_viewer->_shadowMaterial)
        {
            try
            {
                mx::ShaderPtr hwShader = mx::createDepthShader(genContext, stdLib, "__SHADOW_SHADER__");
                _viewer->_shadowMaterial = mx::MslMaterial::create();
                _viewer->_shadowMaterial->generateShader(hwShader);
            }
            catch (std::exception& e)
            {
                std::cerr << "Failed to generate shadow shader: " << e.what() << std::endl;
                _viewer->_shadowMaterial = nullptr;
            }
        }
        if (!_viewer->_shadowBlurMaterial)
        {
            try
            {
                mx::ShaderPtr hwShader = mx::createBlurShader(genContext, stdLib, "__SHADOW_BLUR_SHADER__", "gaussian", 1.0f);
                _viewer->_shadowBlurMaterial = mx::MslMaterial::create();
                _viewer->_shadowBlurMaterial->generateShader(hwShader);
            }
            catch (std::exception& e)
            {
                std::cerr << "Failed to generate shadow blur shader: " << e.what() << std::endl;
                _viewer->_shadowBlurMaterial = nullptr;
            }
        }

        if (_viewer->_shadowMaterial && _viewer->_shadowBlurMaterial)
        {
            bool captureShadowGeneration = false;
            if(captureShadowGeneration)
                MTL_TRIGGER_CAPTURE;
            
            MTL(beginCommandBuffer());
            MTLRenderPassDescriptor* renderpassDesc = [MTLRenderPassDescriptor new];
            _shadowMapFramebuffer->setColorTexture(shadowMapTex[0]);
            _shadowMapFramebuffer->bind(renderpassDesc);
            MTL(beginEncoder(renderpassDesc));
            [MTL(renderCmdEncoder) setDepthStencilState:MTL_DEPTHSTENCIL_STATE(opaque)];

            // Render shadow geometry.
            _viewer->_shadowMaterial->bindShader();
            for (auto mesh : _viewer->_geometryHandler->getMeshes())
            {
                _viewer->_shadowMaterial->bindMesh(mesh);
                _viewer->_shadowMaterial->bindViewInformation(shadowCamera);
                std::static_pointer_cast<mx::MslMaterial>
                (_viewer->_shadowMaterial)->prepareUsedResources(
                            shadowCamera,
                            geometryHandler,
                            imageHandler,
                            lightHandler);
                for (size_t i = 0; i < mesh->getPartitionCount(); i++)
                {
                    mx::MeshPartitionPtr geom = mesh->getPartition(i);
                    _viewer->_shadowMaterial->drawPartition(geom);
                }
            }
            
            MTL(endCommandBuffer());
            
            // Apply Gaussian blurring.
            mx::ImageSamplingProperties blurSamplingProperties;
            blurSamplingProperties.uaddressMode = mx::ImageSamplingProperties::AddressMode::CLAMP;
            blurSamplingProperties.vaddressMode = mx::ImageSamplingProperties::AddressMode::CLAMP;
            blurSamplingProperties.filterType = mx::ImageSamplingProperties::FilterType::CLOSEST;
            for (unsigned int i = 0; i < _viewer->_shadowSoftness; i++)
            {
                MTL(beginCommandBuffer());
                _shadowMapFramebuffer->setColorTexture(shadowMapTex[(i+1) % 2]);
                _shadowMapFramebuffer->bind(renderpassDesc);
                MTL(beginEncoder(renderpassDesc));
                _shadowMapFramebuffer->bind(renderpassDesc);
                _viewer->_shadowBlurMaterial->bindShader();
                std::static_pointer_cast<mx::MslMaterial>
                (_viewer->_shadowBlurMaterial)->getProgram()->bindTexture(
                    _viewer->_imageHandler,
                    "image_file_tex",
                    _shadowMap[i % 2],
                    blurSamplingProperties);
                std::static_pointer_cast<mx::MslMaterial>
                (_viewer->_shadowBlurMaterial)->prepareUsedResources(
                    identityCamera,
                    geometryHandler,
                    imageHandler,
                    lightHandler);
                _viewer->_shadowBlurMaterial->unbindGeometry();
                _viewer->renderScreenSpaceQuad(_viewer->_shadowBlurMaterial);
                MTL(endCommandBuffer());
            }
            
            MTL_POP_FRAMEBUFFER();
            if(captureShadowGeneration)
                MTL_STOP_CAPTURE;
            
            [renderpassDesc release];
        }

        // Reset frame timing after shadow generation.
        _viewer->resetFrameTiming();
    }

    _viewer->_shadowMap = _shadowMap[_viewer->_shadowSoftness % 2];
    return _viewer->_shadowMap;
}

void MetalRenderPipeline::renderFrame(void* color_texture, int shadowMapSize, const char* dirLightNodeCat)
{
    auto& genContext    = _viewer->_genContext;
    auto& lightHandler  = _viewer->_lightHandler;
    auto& imageHandler  = _viewer->_imageHandler;
    auto& viewCamera    = _viewer->_viewCamera;
    auto& envCamera     = _viewer->_envCamera;
    auto& shadowCamera  = _viewer->_shadowCamera;
    float lightRotation = _viewer->_lightRotation;
    auto& searchPath    = _viewer->_searchPath;
    auto& geometryHandler    = _viewer->_geometryHandler;
    
    // Update prefiltered environment.
    if (lightHandler->getUsePrefilteredMap() && !_viewer->_materialAssignments.empty())
    {
        updatePrefilteredMap();
    }

    // Update lighting state.
    lightHandler->setLightTransform(mx::Matrix44::createRotationY(lightRotation / 180.0f * M_PI));

    // Update shadow state.
    mx::ShadowState shadowState;
    shadowState.ambientOcclusionGain = _viewer->_ambientOcclusionGain;
    mx::NodePtr dirLight = lightHandler->getFirstLightOfCategory(dirLightNodeCat);
    if (genContext.getOptions().hwShadowMap && dirLight)
    {
        mx::ImagePtr shadowMap = getShadowMap(shadowMapSize);
        if (shadowMap)
        {
            shadowState.shadowMap = shadowMap;
            shadowState.shadowMatrix = viewCamera->getWorldMatrix().getInverse() *
                shadowCamera->getWorldViewProjMatrix();
        }
        else
        {
            genContext.getOptions().hwShadowMap = false;
        }
    }
    
    bool captureFrame = false;
    if(captureFrame)
        MTL_TRIGGER_CAPTURE;
    
    bool useTiledPipeline;
    if(@available(macOS 11.0, ios 14.0, *))
    {
        useTiledPipeline = MTL(supportsTiledPipeline);
    }
    else
    {
        useTiledPipeline = false;
    }
    
    MTL(beginCommandBuffer());
    std::unique_ptr<void, void(*)(void *)> guard((void *)1, [](void *) {
        // Clean up Metal objects in the event of an exception.
        MTL(endCommandBuffer());
    });

    MTLRenderPassDescriptor* renderpassDesc = [MTLRenderPassDescriptor new];
    if(useTiledPipeline)
    {
        [renderpassDesc.colorAttachments[0] setTexture:(id<MTLTexture>)color_texture];
    }
    else
    {
        [renderpassDesc.colorAttachments[0] setTexture:MTL(currentFramebuffer())->getColorTexture()];
    }
    [renderpassDesc.colorAttachments[0] setClearColor:MTLClearColorMake(
                                        _viewer->m_background[0],
                                        _viewer->m_background[1],
                                        _viewer->m_background[2],
                                        _viewer->m_background[3])];
    [renderpassDesc.colorAttachments[0] setLoadAction:MTLLoadActionClear];
    [renderpassDesc.colorAttachments[0] setStoreAction:MTLStoreActionStore];
    
    [renderpassDesc.depthAttachment setTexture:MTL(currentFramebuffer())->getDepthTexture()];
    [renderpassDesc.depthAttachment setClearDepth:1.0];
    [renderpassDesc.depthAttachment setLoadAction:MTLLoadActionClear];
    [renderpassDesc.depthAttachment setStoreAction:MTLStoreActionStore];
    [renderpassDesc setStencilAttachment:nil];
        
    MTL(beginEncoder(renderpassDesc));
        
    [MTL(renderCmdEncoder) setFrontFacingWinding:MTLWindingClockwise];

    // Environment background
    if (_viewer->_drawEnvironment)
    {
        [MTL(renderCmdEncoder) setDepthStencilState:MTL_DEPTHSTENCIL_STATE(envMap)];
        mx::MslMaterialPtr envMaterial = std::static_pointer_cast<mx::MslMaterial>(_viewer->getEnvironmentMaterial());
        if (envMaterial)
        {
            const mx::MeshList& meshes = _viewer->_envGeometryHandler->getMeshes();
            mx::MeshPartitionPtr envPart = !meshes.empty() ? meshes[0]->getPartition(0) : nullptr;
            if (envPart)
            {
                // Apply rotation to the environment shader.
                envMaterial->modifyUniform("envImage/rotation", mx::Value::createValue(lightRotation));

                // Apply light intensity to the environment shader.
                envMaterial->modifyUniform("envImageAdjusted/in2", mx::Value::createValue(lightHandler->getEnvLightIntensity()));

                // Render the environment mesh.
                [MTL(renderCmdEncoder) setCullMode:MTLCullModeNone];
                envMaterial->bindShader();
                envMaterial->bindMesh(meshes[0]);
                envMaterial->bindViewInformation(envCamera);
                envMaterial->bindImages(imageHandler, searchPath, false);
                envMaterial->prepareUsedResources(envCamera,
                                        _viewer->_envGeometryHandler,
                                        imageHandler,
                                        lightHandler);
                envMaterial->drawPartition(envPart);
                [MTL(renderCmdEncoder) setCullMode:MTLCullModeNone];
            }
        }
        else
        {
            _viewer->_drawEnvironment = false;
        }
    }

    // Enable backface culling if requested.
    if (!_viewer->_renderDoubleSided)
    {
        [MTL(renderCmdEncoder) setCullMode:MTLCullModeBack];
    }

    // Opaque pass
    [MTL(renderCmdEncoder) setDepthStencilState:MTL_DEPTHSTENCIL_STATE(opaque)];
    for (const auto& assignment : _viewer->_materialAssignments)
    {
        mx::MeshPartitionPtr geom = assignment.first;
        mx::MslMaterialPtr material = std::static_pointer_cast<mx::MslMaterial>(assignment.second);
        shadowState.ambientOcclusionMap = _viewer->getAmbientOcclusionImage(material);
        if (!material)
        {
            continue;
        }

        material->bindShader();
        material->bindMesh(_viewer->_geometryHandler->findParentMesh(geom));
        if (material->getProgram()->hasUniform(mx::HW::ALPHA_THRESHOLD))
        {
            material->getProgram()->bindUniform(mx::HW::ALPHA_THRESHOLD, mx::Value::createValue(0.99f));
        }
        material->getProgram()->bindTimeAndFrame((float) _timer.elapsedTime(), (float) _frame);
        material->bindViewInformation(viewCamera);
        material->bindLighting(lightHandler, imageHandler, shadowState);
        material->bindImages(imageHandler, _viewer->_searchPath);
        material->prepareUsedResources(viewCamera,
                             geometryHandler,
                             imageHandler,
                             lightHandler);
        material->drawPartition(geom);
        material->unbindImages(imageHandler);
    }

    // Transparent pass
    if (_viewer->_renderTransparency)
    {
        [MTL(renderCmdEncoder) setDepthStencilState:MTL_DEPTHSTENCIL_STATE(transparent)];
        for (const auto& assignment : _viewer->_materialAssignments)
        {
            mx::MeshPartitionPtr geom = assignment.first;
            mx::MslMaterialPtr material = std::static_pointer_cast<mx::MslMaterial>(assignment.second);
            shadowState.ambientOcclusionMap = _viewer->getAmbientOcclusionImage(material);
            if (!material || !material->hasTransparency())
            {
                continue;
            }

            material->bindShader();
            material->bindMesh(geometryHandler->findParentMesh(geom));
            if (material->getProgram()->hasUniform(mx::HW::ALPHA_THRESHOLD))
            {
                material->getProgram()->bindUniform(mx::HW::ALPHA_THRESHOLD, mx::Value::createValue(0.001f));
            }
            material->getProgram()->bindTimeAndFrame((float) _timer.elapsedTime(), (float) _frame);
            material->bindViewInformation(viewCamera);
            material->bindLighting(lightHandler, imageHandler, shadowState);
            material->bindImages(imageHandler, searchPath);
            material->prepareUsedResources(viewCamera,
                                 geometryHandler,
                                 imageHandler,
                                 lightHandler);
            material->drawPartition(geom);
            material->unbindImages(imageHandler);
        }
    }

    if (!_viewer->_renderDoubleSided)
    {
        [MTL(renderCmdEncoder) setCullMode:MTLCullModeNone];
    }

    // Wireframe pass
    if (_viewer->_outlineSelection)
    {
        mx::MslMaterialPtr wireMaterial =
            std::static_pointer_cast<mx::MslMaterial>(_viewer->getWireframeMaterial());
        if (wireMaterial)
        {
            [MTL(renderCmdEncoder) setCullMode:MTLCullModeNone];
            [MTL(renderCmdEncoder) setTriangleFillMode:MTLTriangleFillModeLines];
            wireMaterial->bindShader();
            wireMaterial->bindMesh(geometryHandler->findParentMesh(_viewer->getSelectedGeometry()));
            wireMaterial->bindViewInformation(viewCamera);
            wireMaterial->prepareUsedResources(viewCamera,
                                 geometryHandler,
                                 imageHandler,
                                 lightHandler);
            wireMaterial->drawPartition(_viewer->getSelectedGeometry());
            [MTL(renderCmdEncoder) setTriangleFillMode:MTLTriangleFillModeFill];
            [MTL(renderCmdEncoder) setCullMode:MTLCullModeNone];
        }
        else
        {
            _viewer->_outlineSelection = false;
        }
    }
    
#ifdef MAC_OS_VERSION_11_0
    if(useTiledPipeline)
    {
        if(@available(macOS 11.0, ios 14.0, *))
        {
            [MTL(renderCmdEncoder) setRenderPipelineState:MTL(linearToSRGB_pso)];
            [MTL(renderCmdEncoder) dispatchThreadsPerTile:MTLSizeMake(
                                        MTL(renderCmdEncoder).tileWidth,
                                        MTL(renderCmdEncoder).tileHeight, 1)];
        }
    }
    
    if(!useTiledPipeline)
#endif
    {
        MTL(endEncoder());
        [renderpassDesc.colorAttachments[0] setTexture:(id<MTLTexture>)color_texture];
        MTL(beginEncoder(renderpassDesc));
        [MTL(renderCmdEncoder) setRenderPipelineState:MTL(linearToSRGB_pso)];
        [MTL(renderCmdEncoder)
            setFragmentTexture:MTL(currentFramebuffer())->getColorTexture()
            atIndex:0];
        [MTL(renderCmdEncoder) drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
    }

    guard.release();
    MTL(endCommandBuffer());
    
    if(captureFrame)
        MTL_STOP_CAPTURE;
    
    [renderpassDesc release];
}

void MetalRenderPipeline::bakeTextures()
{
    auto& imageHandler = _viewer->_imageHandler;
    
    mx::MaterialPtr material = _viewer->getSelectedMaterial();
    mx::DocumentPtr doc = material ? material->getDocument() : nullptr;
    if (!doc)
    {
        return;
    }

    {
        // Construct a texture baker.
        mx::Image::BaseType baseType = _viewer->_bakeHdr ? mx::Image::BaseType::FLOAT : mx::Image::BaseType::UINT8;
        mx::UnsignedIntPair bakingRes = _viewer->computeBakingResolution(doc);
        mx::TextureBakerPtr baker = std::static_pointer_cast<mx::TextureBakerPtr::element_type>(createTextureBaker(bakingRes.first, bakingRes.second, baseType));
        baker->setupUnitSystem(_viewer->_stdLib);
        baker->setDistanceUnit(_viewer->_genContext.getOptions().targetDistanceUnit);
        baker->setAverageImages(_viewer->_bakeAverage);
        baker->setOptimizeConstants(_viewer->_bakeOptimize);
        baker->writeDocumentPerMaterial(_viewer->_bakeDocumentPerMaterial);

        // Assign our existing image handler, releasing any existing render resources for cached images.
        imageHandler->releaseRenderResources();
        baker->setImageHandler(imageHandler);

        // Extend the image search path to include material source folders.
        mx::FileSearchPath extendedSearchPath = _viewer->_searchPath;
        extendedSearchPath.append(_viewer->_materialSearchPath);

        // Bake all materials in the active document.
        try
        {
            baker->bakeAllMaterials(doc, extendedSearchPath, _viewer->_bakeFilename);
        }
        catch (std::exception& e)
        {
            std::cerr << "Error in texture baking: " << e.what() << std::endl;
        }

        // Release any render resources generated by the baking process.
        imageHandler->releaseRenderResources();
    }
}

mx::ImagePtr MetalRenderPipeline::getFrameImage()
{
    unsigned int width = MTL(currentFramebuffer())->getWidth();
    unsigned int height = MTL(currentFramebuffer())->getHeight();
    
    MTL(waitForCompletion());

    id<MTLTexture> srcTexture = MTL(supportsTiledPipeline) ?
                                    (id<MTLTexture>)_viewer->_colorTexture :
                                    MTL(currentFramebuffer())->getColorTexture();

    mx::MetalFramebufferPtr framebuffer = mx::MetalFramebuffer::create(
                            MTL(device),
                            width, height, 4,
                            mx::Image::BaseType::UINT8,
                            nil,
                            false, MTLPixelFormatBGRA8Unorm);
    id<MTLTexture> dstTexture = framebuffer->getColorTexture();

    id<MTLCommandQueue> cmdQueue = MTL(cmdQueue);

    // Copy with format conversion
    MPSImageConversion* conversion = [[MPSImageConversion alloc] initWithDevice:MTL(device)];
    id<MTLCommandBuffer> cmdBuffer = [cmdQueue commandBuffer];
    [conversion encodeToCommandBuffer:cmdBuffer sourceTexture:srcTexture destinationTexture:dstTexture];
    [cmdBuffer commit];
    [cmdBuffer waitUntilCompleted];

    mx::ImagePtr frame = framebuffer->getColorImage(cmdQueue);
    
    // Flips the captured image
    std::vector<unsigned char> tmp(frame->getRowStride());
    unsigned int half_height = height / 2;
    unsigned char* resourceBuffer = static_cast<unsigned char*>(frame->getResourceBuffer());
    for(unsigned int i = 0; i < half_height; ++i)
    {
        memcpy(tmp.data(),
               &resourceBuffer[i*frame->getRowStride()], frame->getRowStride());
        memcpy(&resourceBuffer[i*frame->getRowStride()],
               &resourceBuffer[(height - i - 1) * frame->getRowStride()], frame->getRowStride());
        memcpy(&resourceBuffer[(height - i - 1) * frame->getRowStride()],
               tmp.data(), frame->getRowStride());
    }
    
    framebuffer = nullptr;
    return frame;
}
