void RenderHeatmapLayer::render(PaintParameters& parameters, RenderSource*) {
    if (parameters.pass == RenderPass::Opaque) {
        return;
    }

    if (parameters.pass == RenderPass::Pass3D) {
        const auto& viewportSize = parameters.staticData.backendSize;
        const auto size = Size{viewportSize.width / 4, viewportSize.height / 4};

        if (!renderTexture || renderTexture->getSize() != size) {
            if (parameters.context.supportsHalfFloatTextures) {
                renderTexture = OffscreenTexture(parameters.context, size, gl::TextureType::HalfFloat);

                try {
                    renderTexture->bind();
                } catch (const std::runtime_error& ex) {
                    // can't render to a half-float texture; falling back to unsigned byte one
                    renderTexture = nullopt;
                    parameters.context.supportsHalfFloatTextures = false;
                }
            }

            if (!parameters.context.supportsHalfFloatTextures || !renderTexture) {
                renderTexture = OffscreenTexture(parameters.context, size, gl::TextureType::UnsignedByte);
                renderTexture->bind();
            }

        } else {
            renderTexture->bind();
        }

        if (!colorRampTexture) {
            colorRampTexture = parameters.context.createTexture(colorRamp, 1, gl::TextureType::UnsignedByte);
        }

        parameters.context.clear(Color{ 0.0f, 0.0f, 0.0f, 1.0f }, {}, {});

        for (const RenderTile& tile : renderTiles) {
            auto bucket_ = tile.tile.getBucket<HeatmapBucket>(*baseImpl);
            if (!bucket_) {
                continue;
            }
            HeatmapBucket& bucket = *bucket_;

            const auto extrudeScale = tile.id.pixelsToTileUnits(1, parameters.state.getZoom());

            const auto stencilMode = parameters.mapMode != MapMode::Continuous
                ? parameters.stencilModeForClipping(tile.clip)
                : gl::StencilMode::disabled();

            const auto& paintPropertyBinders = bucket.paintPropertyBinders.at(getID());

            auto& programInstance = parameters.programs.heatmap.get(evaluated);
       
            const auto allUniformValues = programInstance.computeAllUniformValues(
                HeatmapProgram::UniformValues {
                    uniforms::u_intensity::Value( evaluated.get<style::HeatmapIntensity>() ),
                    uniforms::u_matrix::Value( tile.matrix ),
                    uniforms::heatmap::u_extrude_scale::Value( extrudeScale )
                },
                paintPropertyBinders,
                evaluated,
                parameters.state.getZoom()
            );
            const auto allAttributeBindings = programInstance.computeAllAttributeBindings(
                *bucket.vertexBuffer,
                paintPropertyBinders,
                evaluated
            );

            checkRenderability(parameters, programInstance.activeBindingCount(allAttributeBindings));

            programInstance.draw(
                parameters.context,
                gl::Triangles(),
                parameters.depthModeForSublayer(0, gl::DepthMode::ReadOnly),
                stencilMode,
                gl::ColorMode::additive(),
                gl::CullFaceMode::disabled(),
                *bucket.indexBuffer,
                bucket.segments,
                allUniformValues,
                allAttributeBindings,
                getID()
            );
        }

    } else if (parameters.pass == RenderPass::Translucent) {
        parameters.context.bindTexture(renderTexture->getTexture(), 0, gl::TextureFilter::Linear);
        parameters.context.bindTexture(*colorRampTexture, 1, gl::TextureFilter::Linear);

        const auto& size = parameters.staticData.backendSize;

        mat4 viewportMat;
        matrix::ortho(viewportMat, 0, size.width, size.height, 0, 0, 1);

        const Properties<>::PossiblyEvaluated properties;
        const HeatmapTextureProgram::PaintPropertyBinders paintAttributeData{ properties, 0 };

        auto& programInstance = parameters.programs.heatmapTexture;

        const auto allUniformValues = programInstance.computeAllUniformValues(
            HeatmapTextureProgram::UniformValues{
                uniforms::u_matrix::Value( viewportMat ), uniforms::u_world::Value( size ),
                uniforms::u_image::Value( 0 ),
                uniforms::u_color_ramp::Value( 1 ),
                uniforms::u_opacity::Value( evaluated.get<HeatmapOpacity>() )
            },
            paintAttributeData,
            properties,
            parameters.state.getZoom()
        );
        const auto allAttributeBindings = programInstance.computeAllAttributeBindings(
            parameters.staticData.extrusionTextureVertexBuffer,
            paintAttributeData,
            properties
        );

        checkRenderability(parameters, programInstance.activeBindingCount(allAttributeBindings));

        programInstance.draw(
            parameters.context,
            gl::Triangles(),
            gl::DepthMode::disabled(),
            gl::StencilMode::disabled(),
            parameters.colorModeForRenderPass(),
            gl::CullFaceMode::disabled(),
            parameters.staticData.quadTriangleIndexBuffer,
            parameters.staticData.extrusionTextureSegments,
            allUniformValues,
            allAttributeBindings,
            getID()
        );
    }
}
void RenderRasterLayer::render(PaintParameters& parameters, RenderSource* source) {
    if (parameters.pass != RenderPass::Translucent)
        return;

    auto draw = [&] (const mat4& matrix,
                     const auto& vertexBuffer,
                     const auto& indexBuffer,
                     const auto& segments) {
        parameters.programs.raster.draw(
            parameters.context,
            gl::Triangles(),
            parameters.depthModeForSublayer(0, gl::DepthMode::ReadOnly),
            gl::StencilMode::disabled(),
            parameters.colorModeForRenderPass(),
            RasterProgram::UniformValues {
                uniforms::u_matrix::Value{ matrix },
                uniforms::u_image0::Value{ 0 },
                uniforms::u_image1::Value{ 1 },
                uniforms::u_opacity::Value{ evaluated.get<RasterOpacity>() },
                uniforms::u_fade_t::Value{ 1 },
                uniforms::u_brightness_low::Value{ evaluated.get<RasterBrightnessMin>() },
                uniforms::u_brightness_high::Value{ evaluated.get<RasterBrightnessMax>() },
                uniforms::u_saturation_factor::Value{ saturationFactor(evaluated.get<RasterSaturation>()) },
                uniforms::u_contrast_factor::Value{ contrastFactor(evaluated.get<RasterContrast>()) },
                uniforms::u_spin_weights::Value{ spinWeights(evaluated.get<RasterHueRotate>()) },
                uniforms::u_buffer_scale::Value{ 1.0f },
                uniforms::u_scale_parent::Value{ 1.0f },
                uniforms::u_tl_parent::Value{ std::array<float, 2> {{ 0.0f, 0.0f }} },
            },
            vertexBuffer,
            indexBuffer,
            segments,
            RasterProgram::PaintPropertyBinders { evaluated, 0 },
            evaluated,
            parameters.state.getZoom(),
            getID()
        );
    };

    if (RenderImageSource* imageSource = source->as<RenderImageSource>()) {
        if (imageSource->isEnabled() && imageSource->isLoaded() && !imageSource->bucket->needsUpload()) {
            RasterBucket& bucket = *imageSource->bucket;

            assert(bucket.texture);
            parameters.context.bindTexture(*bucket.texture, 0, gl::TextureFilter::Linear);
            parameters.context.bindTexture(*bucket.texture, 1, gl::TextureFilter::Linear);

            for (auto matrix_ : imageSource->matrices) {
                draw(matrix_,
                     *bucket.vertexBuffer,
                     *bucket.indexBuffer,
                     bucket.segments);
            }
        }
    } else {
        for (const RenderTile& tile : renderTiles) {
            assert(dynamic_cast<RasterBucket*>(tile.tile.getBucket(*baseImpl)));
            RasterBucket& bucket = *reinterpret_cast<RasterBucket*>(tile.tile.getBucket(*baseImpl));

            if (!bucket.hasData())
                continue;

            assert(bucket.texture);
            parameters.context.bindTexture(*bucket.texture, 0, gl::TextureFilter::Linear);
            parameters.context.bindTexture(*bucket.texture, 1, gl::TextureFilter::Linear);

            if (bucket.vertexBuffer && bucket.indexBuffer && !bucket.segments.empty()) {
                // Draw only the parts of the tile that aren't drawn by another tile in the layer.
                draw(tile.matrix,
                     *bucket.vertexBuffer,
                     *bucket.indexBuffer,
                     bucket.segments);
            } else {
                // Draw the full tile.
                draw(tile.matrix,
                     parameters.staticData.rasterVertexBuffer,
                     parameters.staticData.quadTriangleIndexBuffer,
                     parameters.staticData.rasterSegments);
            }
        }
    }
}
void RenderRasterLayer::render(PaintParameters& parameters) {
    if (parameters.pass != RenderPass::Translucent)
        return;
    const auto& evaluated = static_cast<const RasterLayerProperties&>(*evaluatedProperties).evaluated;
    RasterProgram::Binders paintAttributeData{ evaluated, 0 };

    auto draw = [&] (const mat4& matrix,
                     const auto& vertexBuffer,
                     const auto& indexBuffer,
                     const auto& segments,
                     const auto& textureBindings,
                     const std::string& drawScopeID) {
        auto& programInstance = parameters.programs.getRasterLayerPrograms().raster;

        const auto allUniformValues = programInstance.computeAllUniformValues(
            RasterProgram::LayoutUniformValues {
                uniforms::matrix::Value( matrix ),
                uniforms::opacity::Value( evaluated.get<RasterOpacity>() ),
                uniforms::fade_t::Value( 1 ),
                uniforms::brightness_low::Value( evaluated.get<RasterBrightnessMin>() ),
                uniforms::brightness_high::Value( evaluated.get<RasterBrightnessMax>() ),
                uniforms::saturation_factor::Value( saturationFactor(evaluated.get<RasterSaturation>()) ),
                uniforms::contrast_factor::Value( contrastFactor(evaluated.get<RasterContrast>()) ),
                uniforms::spin_weights::Value( spinWeights(evaluated.get<RasterHueRotate>()) ),
                uniforms::buffer_scale::Value( 1.0f ),
                uniforms::scale_parent::Value( 1.0f ),
                uniforms::tl_parent::Value( std::array<float, 2> {{ 0.0f, 0.0f }} ),
            },
            paintAttributeData,
            evaluated,
            parameters.state.getZoom()
        );
        const auto allAttributeBindings = programInstance.computeAllAttributeBindings(
            vertexBuffer,
            paintAttributeData,
            evaluated
        );

        checkRenderability(parameters, programInstance.activeBindingCount(allAttributeBindings));

        programInstance.draw(
            parameters.context,
            *parameters.renderPass,
            gfx::Triangles(),
            parameters.depthModeForSublayer(0, gfx::DepthMaskType::ReadOnly),
            gfx::StencilMode::disabled(),
            parameters.colorModeForRenderPass(),
            gfx::CullFaceMode::disabled(),
            indexBuffer,
            segments,
            allUniformValues,
            allAttributeBindings,
            textureBindings,
            getID() + "/" + drawScopeID
        );
    };

    const gfx::TextureFilterType filter = evaluated.get<RasterResampling>() == RasterResamplingType::Nearest ? gfx::TextureFilterType::Nearest : gfx::TextureFilterType::Linear;

    if (imageData && !imageData->bucket->needsUpload()) {
        RasterBucket& bucket = *imageData->bucket;
        assert(bucket.texture);

        size_t i = 0;
        for (const auto& matrix_ : *imageData->matrices) {
            draw(matrix_,
                *bucket.vertexBuffer,
                *bucket.indexBuffer,
                bucket.segments,
                RasterProgram::TextureBindings{
                    textures::image0::Value{ bucket.texture->getResource(), filter },
                    textures::image1::Value{ bucket.texture->getResource(), filter },
                },
                bucket.drawScopeID + std::to_string(i++));
        }
    } else {
        for (const RenderTile& tile : renderTiles) {
            auto* bucket_ = tile.getBucket(*baseImpl);
            if (!bucket_) {
                continue;
            }
            auto& bucket = static_cast<RasterBucket&>(*bucket_);

            if (!bucket.hasData())
                continue;

            assert(bucket.texture);
            if (bucket.vertexBuffer && bucket.indexBuffer && !bucket.segments.empty()) {
                // Draw only the parts of the tile that aren't drawn by another tile in the layer.
                draw(parameters.matrixForTile(tile.id, true),
                     *bucket.vertexBuffer,
                     *bucket.indexBuffer,
                     bucket.segments,
                     RasterProgram::TextureBindings{
                         textures::image0::Value{ bucket.texture->getResource(), filter },
                         textures::image1::Value{ bucket.texture->getResource(), filter },
                     },
                     bucket.drawScopeID);
            } else {
                // Draw the full tile.
                draw(parameters.matrixForTile(tile.id, true),
                     *parameters.staticData.rasterVertexBuffer,
                     *parameters.staticData.quadTriangleIndexBuffer,
                     parameters.staticData.rasterSegments,
                     RasterProgram::TextureBindings{
                         textures::image0::Value{ bucket.texture->getResource(), filter },
                         textures::image1::Value{ bucket.texture->getResource(), filter },
                     },
                     bucket.drawScopeID);
            }
        }
    }
}
void RenderHillshadeLayer::render(PaintParameters& parameters) {
    if (parameters.pass != RenderPass::Translucent && parameters.pass != RenderPass::Pass3D)
        return;
    const auto& evaluated = static_cast<const HillshadeLayerProperties&>(*evaluatedProperties).evaluated;  
    auto draw = [&] (const mat4& matrix,
                     const auto& vertexBuffer,
                     const auto& indexBuffer,
                     const auto& segments,
                     const UnwrappedTileID& id,
                     const auto& textureBindings) {
        auto& programInstance = parameters.programs.getHillshadeLayerPrograms().hillshade;

        const HillshadeProgram::Binders paintAttributeData{ evaluated, 0 };

        const auto allUniformValues = programInstance.computeAllUniformValues(
            HillshadeProgram::LayoutUniformValues {
                uniforms::matrix::Value( matrix ),
                uniforms::highlight::Value( evaluated.get<HillshadeHighlightColor>() ),
                uniforms::shadow::Value( evaluated.get<HillshadeShadowColor>() ),
                uniforms::accent::Value( evaluated.get<HillshadeAccentColor>() ),
                uniforms::light::Value( getLight(parameters) ),
                uniforms::latrange::Value( getLatRange(id) ),
            },
            paintAttributeData,
            evaluated,
            parameters.state.getZoom()
        );
        const auto allAttributeBindings = programInstance.computeAllAttributeBindings(
            vertexBuffer,
            paintAttributeData,
            evaluated
        );

        checkRenderability(parameters, programInstance.activeBindingCount(allAttributeBindings));

        programInstance.draw(
            parameters.context,
            *parameters.renderPass,
            gfx::Triangles(),
            parameters.depthModeForSublayer(0, gfx::DepthMaskType::ReadOnly),
            gfx::StencilMode::disabled(),
            parameters.colorModeForRenderPass(),
            gfx::CullFaceMode::disabled(),
            indexBuffer,
            segments,
            allUniformValues,
            allAttributeBindings,
            textureBindings,
            getID()
        );
    };

    mat4 mat;
    matrix::ortho(mat, 0, util::EXTENT, -util::EXTENT, 0, 0, 1);
    matrix::translate(mat, mat, 0, -util::EXTENT, 0);

    for (const RenderTile& tile : renderTiles) {
        auto* bucket_ = tile.getBucket(*baseImpl);
        if (!bucket_) {
            continue;
        }
        auto& bucket = static_cast<HillshadeBucket&>(*bucket_);

        if (!bucket.hasData()){
            continue;
        }

        if (!bucket.isPrepared() && parameters.pass == RenderPass::Pass3D) {
            assert(bucket.dem);
            const uint16_t stride = bucket.getDEMData().stride;
            const uint16_t tilesize = bucket.getDEMData().dim;
            auto view = parameters.context.createOffscreenTexture({ tilesize, tilesize });

            auto renderPass = parameters.encoder->createRenderPass(
                "hillshade prepare", { *view, Color{ 0.0f, 0.0f, 0.0f, 0.0f }, {}, {} });

            const Properties<>::PossiblyEvaluated properties;
            const HillshadePrepareProgram::Binders paintAttributeData{ properties, 0 };
            
            auto& programInstance = parameters.programs.getHillshadeLayerPrograms().hillshadePrepare;

            const auto allUniformValues = programInstance.computeAllUniformValues(
                HillshadePrepareProgram::LayoutUniformValues {
                    uniforms::matrix::Value( mat ),
                    uniforms::dimension::Value( {{stride, stride}} ),
                    uniforms::zoom::Value( float(tile.id.canonical.z) ),
                    uniforms::maxzoom::Value( float(maxzoom) ),
                },
                paintAttributeData,
                properties,
                parameters.state.getZoom()
            );
            const auto allAttributeBindings = programInstance.computeAllAttributeBindings(
                *parameters.staticData.rasterVertexBuffer,
                paintAttributeData,
                properties
            );

            checkRenderability(parameters, programInstance.activeBindingCount(allAttributeBindings));

            programInstance.draw(
                parameters.context,
                *renderPass,
                gfx::Triangles(),
                parameters.depthModeForSublayer(0, gfx::DepthMaskType::ReadOnly),
                gfx::StencilMode::disabled(),
                parameters.colorModeForRenderPass(),
                gfx::CullFaceMode::disabled(),
                *parameters.staticData.quadTriangleIndexBuffer,
                parameters.staticData.rasterSegments,
                allUniformValues,
                allAttributeBindings,
                HillshadePrepareProgram::TextureBindings{
                    textures::image::Value{ bucket.dem->getResource() },
                },
                getID()
            );
            bucket.texture = std::move(view->getTexture());
            bucket.setPrepared(true);
        } else if (parameters.pass == RenderPass::Translucent) {
            assert(bucket.texture);

            if (bucket.vertexBuffer && bucket.indexBuffer && !bucket.segments.empty()) {
                // Draw only the parts of the tile that aren't drawn by another tile in the layer.
                draw(parameters.matrixForTile(tile.id, true),
                     *bucket.vertexBuffer,
                     *bucket.indexBuffer,
                     bucket.segments,
                     tile.id,
                     HillshadeProgram::TextureBindings{
                         textures::image::Value{ bucket.texture->getResource(), gfx::TextureFilterType::Linear },
                     });
            } else {
                // Draw the full tile.
                draw(parameters.matrixForTile(tile.id, true),
                     *parameters.staticData.rasterVertexBuffer,
                     *parameters.staticData.quadTriangleIndexBuffer,
                     parameters.staticData.rasterSegments,
                     tile.id,
                     HillshadeProgram::TextureBindings{
                         textures::image::Value{ bucket.texture->getResource(), gfx::TextureFilterType::Linear },
                     });
            }
        }
        

    }
}
void RenderTile::finishRender(PaintParameters& parameters) {
    if (!used || parameters.debugOptions == MapDebugOptions::NoDebug)
        return;

    static const style::Properties<>::PossiblyEvaluated properties {};
    static const DebugProgram::PaintPropertyBinders paintAttributeData(properties, 0);

    auto& program = parameters.programs.debug;

    if (parameters.debugOptions & (MapDebugOptions::Timestamps | MapDebugOptions::ParseStatus)) {
        if (!tile.debugBucket || tile.debugBucket->renderable != tile.isRenderable() ||
            tile.debugBucket->complete != tile.isComplete() ||
            !(tile.debugBucket->modified == tile.modified) ||
            !(tile.debugBucket->expires == tile.expires) ||
            tile.debugBucket->debugMode != parameters.debugOptions) {
            tile.debugBucket = std::make_unique<DebugBucket>(
                tile.id, tile.isRenderable(), tile.isComplete(), tile.modified,
                tile.expires, parameters.debugOptions, parameters.context);
        }

        const auto allAttributeBindings = program.computeAllAttributeBindings(
            *tile.debugBucket->vertexBuffer,
            paintAttributeData,
            properties
        );

        program.draw(
            parameters.context,
            gl::Lines { 4.0f * parameters.pixelRatio },
            gl::DepthMode::disabled(),
            parameters.stencilModeForClipping(clip),
            gl::ColorMode::unblended(),
            gl::CullFaceMode::disabled(),
            *tile.debugBucket->indexBuffer,
            tile.debugBucket->segments,
            program.computeAllUniformValues(
                DebugProgram::UniformValues {
                    uniforms::u_matrix::Value( matrix ),
                    uniforms::u_color::Value( Color::white() )
                },
                paintAttributeData,
                properties,
                parameters.state.getZoom()
            ),
            allAttributeBindings,
            "debug"
        );

        program.draw(
            parameters.context,
            gl::Lines { 2.0f * parameters.pixelRatio },
            gl::DepthMode::disabled(),
            parameters.stencilModeForClipping(clip),
            gl::ColorMode::unblended(),
            gl::CullFaceMode::disabled(),
            *tile.debugBucket->indexBuffer,
            tile.debugBucket->segments,
            program.computeAllUniformValues(
                DebugProgram::UniformValues {
                    uniforms::u_matrix::Value( matrix ),
                    uniforms::u_color::Value( Color::black() )
                },
                paintAttributeData,
                properties,
                parameters.state.getZoom()
            ),
            allAttributeBindings,
            "debug"
        );
    }

    if (parameters.debugOptions & MapDebugOptions::TileBorders) {
        parameters.programs.debug.draw(
            parameters.context,
            gl::LineStrip { 4.0f * parameters.pixelRatio },
            gl::DepthMode::disabled(),
            parameters.stencilModeForClipping(clip),
            gl::ColorMode::unblended(),
            gl::CullFaceMode::disabled(),
            parameters.staticData.tileBorderIndexBuffer,
            parameters.staticData.tileBorderSegments,
            program.computeAllUniformValues(
                DebugProgram::UniformValues {
                    uniforms::u_matrix::Value( matrix ),
                    uniforms::u_color::Value( Color::red() )
                },
                paintAttributeData,
                properties,
                parameters.state.getZoom()
            ),
            program.computeAllAttributeBindings(
                parameters.staticData.tileVertexBuffer,
                paintAttributeData,
                properties
            ),
            "debug"
        );
    }
}
void RenderFillLayer::render(PaintParameters& parameters, RenderSource*) {
    if (evaluated.get<FillPattern>().from.empty()) {
        for (const RenderTile& tile : renderTiles) {
            assert(dynamic_cast<FillBucket*>(tile.tile.getBucket(*baseImpl)));
            FillBucket& bucket = *reinterpret_cast<FillBucket*>(tile.tile.getBucket(*baseImpl));

            auto draw = [&] (auto& program,
                             const auto& drawMode,
                             const auto& depthMode,
                             const auto& indexBuffer,
                             const auto& segments) {
                program.get(evaluated).draw(
                    parameters.context,
                    drawMode,
                    depthMode,
                    parameters.stencilModeForClipping(tile.clip),
                    parameters.colorModeForRenderPass(),
                    FillProgram::UniformValues {
                        uniforms::u_matrix::Value{
                            tile.translatedMatrix(evaluated.get<FillTranslate>(),
                                                  evaluated.get<FillTranslateAnchor>(),
                                                  parameters.state)
                        },
                        uniforms::u_world::Value{ parameters.context.viewport.getCurrentValue().size },
                    },
                    *bucket.vertexBuffer,
                    indexBuffer,
                    segments,
                    bucket.paintPropertyBinders.at(getID()),
                    evaluated,
                    parameters.state.getZoom(),
                    getID()
                );
            };

            // Only draw the fill when it's opaque and we're drawing opaque fragments,
            // or when it's translucent and we're drawing translucent fragments.
            if ((evaluated.get<FillColor>().constantOr(Color()).a >= 1.0f
              && evaluated.get<FillOpacity>().constantOr(0) >= 1.0f) == (parameters.pass == RenderPass::Opaque)) {
                draw(parameters.programs.fill,
                     gl::Triangles(),
                     parameters.depthModeForSublayer(1, parameters.pass == RenderPass::Opaque
                        ? gl::DepthMode::ReadWrite
                        : gl::DepthMode::ReadOnly),
                     *bucket.triangleIndexBuffer,
                     bucket.triangleSegments);
            }

            if (evaluated.get<FillAntialias>() && parameters.pass == RenderPass::Translucent) {
                draw(parameters.programs.fillOutline,
                     gl::Lines{ 2.0f },
                     parameters.depthModeForSublayer(
                         unevaluated.get<FillOutlineColor>().isUndefined() ? 2 : 0,
                         gl::DepthMode::ReadOnly),
                     *bucket.lineIndexBuffer,
                     bucket.lineSegments);
            }
        }
    } else {
        if (parameters.pass != RenderPass::Translucent) {
            return;
        }

        optional<ImagePosition> imagePosA = parameters.imageManager.getPattern(evaluated.get<FillPattern>().from);
        optional<ImagePosition> imagePosB = parameters.imageManager.getPattern(evaluated.get<FillPattern>().to);

        if (!imagePosA || !imagePosB) {
            return;
        }

        parameters.imageManager.bind(parameters.context, 0);

        for (const RenderTile& tile : renderTiles) {
            assert(dynamic_cast<FillBucket*>(tile.tile.getBucket(*baseImpl)));
            FillBucket& bucket = *reinterpret_cast<FillBucket*>(tile.tile.getBucket(*baseImpl));

            auto draw = [&] (auto& program,
                             const auto& drawMode,
                             const auto& depthMode,
                             const auto& indexBuffer,
                             const auto& segments) {
                program.get(evaluated).draw(
                    parameters.context,
                    drawMode,
                    depthMode,
                    parameters.stencilModeForClipping(tile.clip),
                    parameters.colorModeForRenderPass(),
                    FillPatternUniforms::values(
                        tile.translatedMatrix(evaluated.get<FillTranslate>(),
                                              evaluated.get<FillTranslateAnchor>(),
                                              parameters.state),
                        parameters.context.viewport.getCurrentValue().size,
                        parameters.imageManager.getPixelSize(),
                        *imagePosA,
                        *imagePosB,
                        evaluated.get<FillPattern>(),
                        tile.id,
                        parameters.state
                    ),
                    *bucket.vertexBuffer,
                    indexBuffer,
                    segments,
                    bucket.paintPropertyBinders.at(getID()),
                    evaluated,
                    parameters.state.getZoom(),
                    getID()
                );
            };

            draw(parameters.programs.fillPattern,
                 gl::Triangles(),
                 parameters.depthModeForSublayer(1, gl::DepthMode::ReadWrite),
                 *bucket.triangleIndexBuffer,
                 bucket.triangleSegments);

            if (evaluated.get<FillAntialias>() && unevaluated.get<FillOutlineColor>().isUndefined()) {
                draw(parameters.programs.fillOutlinePattern,
                     gl::Lines { 2.0f },
                     parameters.depthModeForSublayer(2, gl::DepthMode::ReadOnly),
                     *bucket.lineIndexBuffer,
                     bucket.lineSegments);
            }
        }
    }
}
void RenderSymbolLayer::render(PaintParameters& parameters, RenderSource*) {
    if (parameters.pass == RenderPass::Opaque) {
        return;
    }

    for (const RenderTile& tile : renderTiles) {
        assert(dynamic_cast<SymbolBucket*>(tile.tile.getBucket(*baseImpl)));
        SymbolBucket& bucket = *reinterpret_cast<SymbolBucket*>(tile.tile.getBucket(*baseImpl));

        const auto& layout = bucket.layout;

        auto draw = [&] (auto& program,
                         auto&& uniformValues,
                         const auto& buffers,
                         const auto& symbolSizeBinder,
                         const SymbolPropertyValues& values_,
                         const auto& binders,
                         const auto& paintProperties)
        {
            auto& programInstance = program.get(paintProperties);

            const auto allUniformValues = programInstance.computeAllUniformValues(
                std::move(uniformValues),
                *symbolSizeBinder,
                binders,
                paintProperties,
                parameters.state.getZoom()
            );
            const auto allAttributeBindings = programInstance.computeAllAttributeBindings(
                *buffers.vertexBuffer,
                *buffers.dynamicVertexBuffer,
                *buffers.opacityVertexBuffer,
                binders,
                paintProperties
            );

            checkRenderability(parameters, programInstance.activeBindingCount(allAttributeBindings));

            programInstance.draw(
                parameters.context,
                gl::Triangles(),
                values_.pitchAlignment == AlignmentType::Map
                    ? parameters.depthModeForSublayer(0, gl::DepthMode::ReadOnly)
                    : gl::DepthMode::disabled(),
                gl::StencilMode::disabled(),
                parameters.colorModeForRenderPass(),
                *buffers.indexBuffer,
                buffers.segments,
                allUniformValues,
                allAttributeBindings,
                getID()
            );
        };

        assert(dynamic_cast<GeometryTile*>(&tile.tile));
        GeometryTile& geometryTile = static_cast<GeometryTile&>(tile.tile);

        if (bucket.hasIconData()) {
            auto values = iconPropertyValues(layout);
            auto paintPropertyValues = iconPaintProperties();

            const bool alongLine = layout.get<SymbolPlacement>() == SymbolPlacementType::Line &&
                layout.get<IconRotationAlignment>() == AlignmentType::Map;

            if (alongLine) {
                reprojectLineLabels(bucket.icon.dynamicVertices,
                                    bucket.icon.placedSymbols,
                                    tile.matrix,
                                    values,
                                    tile,
                                    *bucket.iconSizeBinder,
                                    parameters.state);

                parameters.context.updateVertexBuffer(*bucket.icon.dynamicVertexBuffer, std::move(bucket.icon.dynamicVertices));
            }

            const bool iconScaled = layout.get<IconSize>().constantOr(1.0) != 1.0 || bucket.iconsNeedLinear;
            const bool iconTransformed = values.rotationAlignment == AlignmentType::Map || parameters.state.getPitch() != 0;

            parameters.context.bindTexture(*geometryTile.iconAtlasTexture, 0,
                bucket.sdfIcons || parameters.state.isChanging() || iconScaled || iconTransformed
                    ? gl::TextureFilter::Linear : gl::TextureFilter::Nearest);

            const Size texsize = geometryTile.iconAtlasTexture->size;

            if (bucket.sdfIcons) {
                if (values.hasHalo) {
                    draw(parameters.programs.symbolIconSDF,
                         SymbolSDFIconProgram::uniformValues(false, values, texsize, parameters.pixelsToGLUnits, alongLine, tile, parameters.state, parameters.symbolFadeChange, SymbolSDFPart::Halo),
                         bucket.icon,
                         bucket.iconSizeBinder,
                         values,
                         bucket.paintPropertyBinders.at(getID()).first,
                         paintPropertyValues);
                }

                if (values.hasFill) {
                    draw(parameters.programs.symbolIconSDF,
                         SymbolSDFIconProgram::uniformValues(false, values, texsize, parameters.pixelsToGLUnits, alongLine, tile, parameters.state, parameters.symbolFadeChange, SymbolSDFPart::Fill),
                         bucket.icon,
                         bucket.iconSizeBinder,
                         values,
                         bucket.paintPropertyBinders.at(getID()).first,
                         paintPropertyValues);
                }
            } else {
                draw(parameters.programs.symbolIcon,
                     SymbolIconProgram::uniformValues(false, values, texsize, parameters.pixelsToGLUnits, alongLine, tile, parameters.state, parameters.symbolFadeChange),
                     bucket.icon,
                     bucket.iconSizeBinder,
                     values,
                     bucket.paintPropertyBinders.at(getID()).first,
                     paintPropertyValues);
            }
        }

        if (bucket.hasTextData()) {
            parameters.context.bindTexture(*geometryTile.glyphAtlasTexture, 0, gl::TextureFilter::Linear);

            auto values = textPropertyValues(layout);
            auto paintPropertyValues = textPaintProperties();

            const bool alongLine = layout.get<SymbolPlacement>() == SymbolPlacementType::Line &&
                layout.get<TextRotationAlignment>() == AlignmentType::Map;

            if (alongLine) {
                reprojectLineLabels(bucket.text.dynamicVertices,
                                    bucket.text.placedSymbols,
                                    tile.matrix,
                                    values,
                                    tile,
                                    *bucket.textSizeBinder,
                                    parameters.state);

                parameters.context.updateVertexBuffer(*bucket.text.dynamicVertexBuffer, std::move(bucket.text.dynamicVertices));
            }

            const Size texsize = geometryTile.glyphAtlasTexture->size;

            if (values.hasHalo) {
                draw(parameters.programs.symbolGlyph,
                     SymbolSDFTextProgram::uniformValues(true, values, texsize, parameters.pixelsToGLUnits, alongLine, tile, parameters.state, parameters.symbolFadeChange, SymbolSDFPart::Halo),
                     bucket.text,
                     bucket.textSizeBinder,
                     values,
                     bucket.paintPropertyBinders.at(getID()).second,
                     paintPropertyValues);
            }

            if (values.hasFill) {
                draw(parameters.programs.symbolGlyph,
                     SymbolSDFTextProgram::uniformValues(true, values, texsize, parameters.pixelsToGLUnits, alongLine, tile, parameters.state, parameters.symbolFadeChange, SymbolSDFPart::Fill),
                     bucket.text,
                     bucket.textSizeBinder,
                     values,
                     bucket.paintPropertyBinders.at(getID()).second,
                     paintPropertyValues);
            }
        }

        if (bucket.hasCollisionBoxData()) {
            static const style::Properties<>::PossiblyEvaluated properties {};
            static const CollisionBoxProgram::PaintPropertyBinders paintAttributeData(properties, 0);

            auto pixelRatio = tile.id.pixelsToTileUnits(1, parameters.state.getZoom());
            const float scale = std::pow(2, parameters.state.getZoom() - tile.tile.id.overscaledZ);
            std::array<float,2> extrudeScale =
                {{
                    parameters.pixelsToGLUnits[0] / (pixelRatio * scale),
                    parameters.pixelsToGLUnits[1] / (pixelRatio * scale)
                    
                }};
            parameters.programs.collisionBox.draw(
                parameters.context,
                gl::Lines { 1.0f },
                gl::DepthMode::disabled(),
                gl::StencilMode::disabled(),
                parameters.colorModeForRenderPass(),
                CollisionBoxProgram::UniformValues {
                    uniforms::u_matrix::Value{ tile.matrix },
                    uniforms::u_extrude_scale::Value{ extrudeScale },
                    uniforms::u_camera_to_center_distance::Value{ parameters.state.getCameraToCenterDistance() }
                },
                *bucket.collisionBox.vertexBuffer,
                *bucket.collisionBox.dynamicVertexBuffer,
                *bucket.collisionBox.indexBuffer,
                bucket.collisionBox.segments,
                paintAttributeData,
                properties,
                parameters.state.getZoom(),
                getID()
            );
        }
        if (bucket.hasCollisionCircleData()) {
            static const style::Properties<>::PossiblyEvaluated properties {};
            static const CollisionBoxProgram::PaintPropertyBinders paintAttributeData(properties, 0);

            auto pixelRatio = tile.id.pixelsToTileUnits(1, parameters.state.getZoom());
            const float scale = std::pow(2, parameters.state.getZoom() - tile.tile.id.overscaledZ);
            std::array<float,2> extrudeScale =
                {{
                    parameters.pixelsToGLUnits[0] / (pixelRatio * scale),
                    parameters.pixelsToGLUnits[1] / (pixelRatio * scale)
                    
                }};

            parameters.programs.collisionCircle.draw(
                parameters.context,
                gl::Triangles(),
                gl::DepthMode::disabled(),
                gl::StencilMode::disabled(),
                parameters.colorModeForRenderPass(),
                CollisionCircleProgram::UniformValues {
                    uniforms::u_matrix::Value{ tile.matrix },
                    uniforms::u_extrude_scale::Value{ extrudeScale },
                    uniforms::u_overscale_factor::Value{ float(tile.tile.id.overscaleFactor()) },
                    uniforms::u_camera_to_center_distance::Value{ parameters.state.getCameraToCenterDistance() }
                },
                *bucket.collisionCircle.vertexBuffer,
                *bucket.collisionCircle.dynamicVertexBuffer,
                *bucket.collisionCircle.indexBuffer,
                bucket.collisionCircle.segments,
                paintAttributeData,
                properties,
                parameters.state.getZoom(),
                getID()
            );

        }
    }
}