void MetalDriver::draw()

in filament/backend/src/metal/MetalDriver.mm [1152:1406]


void MetalDriver::draw(backend::PipelineState ps, Handle<HwRenderPrimitive> rph) {
    ASSERT_PRECONDITION(mContext->currentRenderPassEncoder != nullptr,
            "Attempted to draw without a valid command encoder.");
    auto primitive = handle_cast<MetalRenderPrimitive>(rph);
    auto program = handle_cast<MetalProgram>(ps.program);
    const auto& rs = ps.rasterState;

    // If the material debugger is enabled, avoid fatal (or cascading) errors and that can occur
    // during the draw call when the program is invalid. The shader compile error has already been
    // dumped to the console at this point, so it's fine to simply return early.
    if (FILAMENT_ENABLE_MATDBG && UTILS_UNLIKELY(!program->isValid)) {
        return;
    }

    ASSERT_PRECONDITION(program->isValid, "Attempting to draw with an invalid Metal program.");

    // Pipeline state
    MTLPixelFormat colorPixelFormat[MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT] = { MTLPixelFormatInvalid };
    for (size_t i = 0; i < MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT; i++) {
        const auto& attachment = mContext->currentRenderTarget->getDrawColorAttachment(i);
        if (!attachment) {
            continue;
        }
        colorPixelFormat[i] = attachment.getPixelFormat();
    }
    MTLPixelFormat depthPixelFormat = MTLPixelFormatInvalid;
    const auto& depthAttachment = mContext->currentRenderTarget->getDepthAttachment();
    if (depthAttachment) {
        depthPixelFormat = depthAttachment.getPixelFormat();
    }
    metal::PipelineState pipelineState {
        .vertexFunction = program->vertexFunction,
        .fragmentFunction = program->fragmentFunction,
        .vertexDescription = primitive->vertexDescription,
        .colorAttachmentPixelFormat = {
            colorPixelFormat[0],
            colorPixelFormat[1],
            colorPixelFormat[2],
            colorPixelFormat[3],
            colorPixelFormat[4],
            colorPixelFormat[5],
            colorPixelFormat[6],
            colorPixelFormat[7]
        },
        .depthAttachmentPixelFormat = depthPixelFormat,
        .sampleCount = mContext->currentRenderTarget->getSamples(),
        .blendState = BlendState {
            .blendingEnabled = rs.hasBlending(),
            .rgbBlendOperation = getMetalBlendOperation(rs.blendEquationRGB),
            .alphaBlendOperation = getMetalBlendOperation(rs.blendEquationAlpha),
            .sourceRGBBlendFactor = getMetalBlendFactor(rs.blendFunctionSrcRGB),
            .sourceAlphaBlendFactor = getMetalBlendFactor(rs.blendFunctionSrcAlpha),
            .destinationRGBBlendFactor = getMetalBlendFactor(rs.blendFunctionDstRGB),
            .destinationAlphaBlendFactor = getMetalBlendFactor(rs.blendFunctionDstAlpha)
        },
        .colorWrite = rs.colorWrite
    };
    mContext->pipelineState.updateState(pipelineState);
    if (mContext->pipelineState.stateChanged()) {
        id<MTLRenderPipelineState> pipeline =
                mContext->pipelineStateCache.getOrCreateState(pipelineState);
        assert_invariant(pipeline != nil);
        [mContext->currentRenderPassEncoder setRenderPipelineState:pipeline];
    }

    // Cull mode
    MTLCullMode cullMode = getMetalCullMode(rs.culling);
    mContext->cullModeState.updateState(cullMode);
    if (mContext->cullModeState.stateChanged()) {
        [mContext->currentRenderPassEncoder setCullMode:cullMode];
    }

    // Front face winding
    MTLWinding winding = rs.inverseFrontFaces ? MTLWindingClockwise : MTLWindingCounterClockwise;
    mContext->windingState.updateState(winding);
    if (mContext->windingState.stateChanged()) {
        [mContext->currentRenderPassEncoder setFrontFacingWinding:winding];
    }

    // Set the depth-stencil state, if a state change is needed.
    DepthStencilState depthState;
    if (depthAttachment) {
        depthState.compareFunction = getMetalCompareFunction(rs.depthFunc);
        depthState.depthWriteEnabled = rs.depthWrite;
    }
    mContext->depthStencilState.updateState(depthState);
    if (mContext->depthStencilState.stateChanged()) {
        id<MTLDepthStencilState> state =
                mContext->depthStencilStateCache.getOrCreateState(depthState);
        assert_invariant(state != nil);
        [mContext->currentRenderPassEncoder setDepthStencilState:state];
    }

    if (ps.polygonOffset.constant != 0.0 || ps.polygonOffset.slope != 0.0) {
        [mContext->currentRenderPassEncoder setDepthBias:ps.polygonOffset.constant
                                              slopeScale:ps.polygonOffset.slope
                                                   clamp:0.0];
    }

    // FIXME: implement take ps.scissor into account
    //  must be intersected with viewport (see OpenGLDriver.cpp for implementation details)

    // Bind uniform buffers.
    MetalBuffer* uniformsToBind[Program::BINDING_COUNT] = { nil };
    NSUInteger offsets[Program::BINDING_COUNT] = { 0 };

    enumerateBoundUniformBuffers([&uniformsToBind, &offsets](const UniformBufferState& state,
            MetalBuffer* buffer, uint32_t index) {
        uniformsToBind[index] = buffer;
        offsets[index] = state.offset;
    });
    MetalBuffer::bindBuffers(getPendingCommandBuffer(mContext), mContext->currentRenderPassEncoder,
            0, MetalBuffer::Stage::VERTEX | MetalBuffer::Stage::FRAGMENT, uniformsToBind, offsets,
            Program::BINDING_COUNT);

    // Enumerate all the sampler buffers for the program and check which textures and samplers need
    // to be bound.

    auto getTextureToBind = [this](const SamplerGroup::Sampler* sampler) {
        const auto metalTexture = handle_const_cast<MetalTexture>(sampler->t);
        id<MTLTexture> textureToBind = metalTexture->swizzledTextureView ? metalTexture->swizzledTextureView
                                                                         : metalTexture->texture;
        if (metalTexture->externalImage.isValid()) {
            textureToBind = metalTexture->externalImage.getMetalTextureForDraw();
        }
        return textureToBind;
    };

    auto getSamplerToBind = [this](const SamplerGroup::Sampler* sampler) {
        const auto metalTexture = handle_const_cast<MetalTexture>(sampler->t);
        SamplerState s {
            .samplerParams = sampler->s,
            .minLod = metalTexture->minLod,
            .maxLod = metalTexture->maxLod
        };
        return mContext->samplerStateCache.getOrCreateState(s);
    };

    id<MTLTexture> texturesToBindVertex[backend::MAX_VERTEX_SAMPLER_COUNT] = {};
    id<MTLSamplerState> samplersToBindVertex[backend::MAX_VERTEX_SAMPLER_COUNT] = {};

    enumerateSamplerGroups(program, ShaderType::VERTEX,
            [this, &getTextureToBind, &getSamplerToBind, &texturesToBindVertex, &samplersToBindVertex](
                    const SamplerGroup::Sampler* sampler, uint8_t binding) {
        // We currently only support a max of MAX_VERTEX_SAMPLER_COUNT samplers. Ignore any additional
        // samplers that may be bound.
        if (binding >= backend::MAX_VERTEX_SAMPLER_COUNT) {
            return;
        }

        auto& textureToBind = texturesToBindVertex[binding];
        textureToBind = getTextureToBind(sampler);
        if (!textureToBind) {
            utils::slog.w << "Warning: no texture bound at binding point " << (size_t) binding
                    << " at the vertex shader." << utils::io::endl;
            textureToBind = getOrCreateEmptyTexture(mContext);
        }

        auto& samplerToBind = samplersToBindVertex[binding];
        samplerToBind = getSamplerToBind(sampler);
    });

    // Assign a default sampler to empty slots, in case Filament hasn't bound all samplers.
    // Metal requires all samplers referenced in shaders to be bound.
    for (auto& sampler : samplersToBindVertex) {
        if (!sampler) {
            sampler = mContext->samplerStateCache.getOrCreateState({});
        }
    }

    NSRange vertexSamplerRange = NSMakeRange(0, backend::MAX_VERTEX_SAMPLER_COUNT);
    [mContext->currentRenderPassEncoder setVertexTextures:texturesToBindVertex
                                                withRange:vertexSamplerRange];
    [mContext->currentRenderPassEncoder setVertexSamplerStates:samplersToBindVertex
                                                     withRange:vertexSamplerRange];

    id<MTLTexture> texturesToBindFragment[backend::MAX_FRAGMENT_SAMPLER_COUNT] = {};
    id<MTLSamplerState> samplersToBindFragment[backend::MAX_FRAGMENT_SAMPLER_COUNT] = {};

    enumerateSamplerGroups(program, ShaderType::FRAGMENT,
            [this, &getTextureToBind, &getSamplerToBind, &texturesToBindFragment, &samplersToBindFragment](
                    const SamplerGroup::Sampler* sampler, uint8_t binding) {
        // We currently only support a max of MAX_FRAGMENT_SAMPLER_COUNT samplers. Ignore any additional
        // samplers that may be bound.
        if (binding >= backend::MAX_FRAGMENT_SAMPLER_COUNT) {
            return;
        }

        auto& textureToBind = texturesToBindFragment[binding];
        textureToBind = getTextureToBind(sampler);
        if (!textureToBind) {
            utils::slog.w << "Warning: no texture bound at binding point " << (size_t) binding
                          << " at the fragment shader." << utils::io::endl;
            textureToBind = getOrCreateEmptyTexture(mContext);
        }

        auto& samplerToBind = samplersToBindFragment[binding];
        samplerToBind = getSamplerToBind(sampler);
    });

    // Assign a default sampler to empty slots, in case Filament hasn't bound all samplers.
    // Metal requires all samplers referenced in shaders to be bound.
    for (auto& sampler : samplersToBindFragment) {
        if (!sampler) {
            sampler = mContext->samplerStateCache.getOrCreateState({});
        }
    }

    NSRange fragmentSamplerRange = NSMakeRange(0, backend::MAX_FRAGMENT_SAMPLER_COUNT);
    [mContext->currentRenderPassEncoder setFragmentTextures:texturesToBindFragment
                                                  withRange:fragmentSamplerRange];
    [mContext->currentRenderPassEncoder setFragmentSamplerStates:samplersToBindFragment
                                                       withRange:fragmentSamplerRange];

    // Bind the vertex buffers.

    MetalBuffer* buffers[MAX_VERTEX_BUFFER_COUNT];
    size_t vertexBufferOffsets[MAX_VERTEX_BUFFER_COUNT];
    size_t bufferIndex = 0;

    auto vb = primitive->vertexBuffer;
    for (uint32_t attributeIndex = 0; attributeIndex < vb->attributes.size(); attributeIndex++) {
        const auto& attribute = vb->attributes[attributeIndex];
        if (attribute.buffer == Attribute::BUFFER_UNUSED) {
            continue;
        }

        assert_invariant(vb->buffers[attribute.buffer]);
        buffers[bufferIndex] = vb->buffers[attribute.buffer];
        vertexBufferOffsets[bufferIndex] = attribute.offset;
        bufferIndex++;
    }

    const auto bufferCount = bufferIndex;
    MetalBuffer::bindBuffers(getPendingCommandBuffer(mContext), mContext->currentRenderPassEncoder,
            VERTEX_BUFFER_START, MetalBuffer::Stage::VERTEX, buffers,
            vertexBufferOffsets, bufferCount);

    // Bind the zero buffer, used for missing vertex attributes.
    static const char bytes[16] = { 0 };
    [mContext->currentRenderPassEncoder setVertexBytes:bytes
                                                length:16
                                               atIndex:(VERTEX_BUFFER_START + ZERO_VERTEX_BUFFER)];

    MetalIndexBuffer* indexBuffer = primitive->indexBuffer;

    id<MTLCommandBuffer> cmdBuffer = getPendingCommandBuffer(mContext);
    id<MTLBuffer> metalIndexBuffer = indexBuffer->buffer.getGpuBufferForDraw(cmdBuffer);
    size_t offset = indexBuffer->buffer.getGpuBufferStreamOffset();
    [mContext->currentRenderPassEncoder drawIndexedPrimitives:getMetalPrimitiveType(primitive->type)
                                                   indexCount:primitive->count
                                                    indexType:getIndexType(indexBuffer->elementSize)
                                                  indexBuffer:metalIndexBuffer
                                            indexBufferOffset:primitive->offset + offset];
}