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 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 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);
            }
        }
    }
}