【surfaceflinger源码分析】surface与surfaceflinger之间的关系

news2024/9/30 23:28:39

本篇文章带着以下问题继续分析surfaceflinger的源码:

  1. 什么是surface ? surface与图形数据之间是什么关系?
  2. surface和surfaceflinger之间是什么关系?

Surface定义

先看看Surface这个类的定义,主要是定义了很多与GraphicBuffer相关的操作。

class Surface
    : public ANativeObjectBase<ANativeWindow, Surface, RefBase>
{
public:

    /*
     * creates a Surface from the given IGraphicBufferProducer (which concrete
     * implementation is a BufferQueue).
     *
     * Surface is mainly state-less while it's disconnected, it can be
     * viewed as a glorified IGraphicBufferProducer holder. It's therefore
     * safe to create other Surfaces from the same IGraphicBufferProducer.
     *
     * However, once a Surface is connected, it'll prevent other Surfaces
     * referring to the same IGraphicBufferProducer to become connected and
     * therefore prevent them to be used as actual producers of buffers.
     *
     * the controlledByApp flag indicates that this Surface (producer) is
     * controlled by the application. This flag is used at connect time.
     */
    explicit Surface(const sp<IGraphicBufferProducer>& bufferProducer,
            bool controlledByApp = false);
    ...............................;
    virtual int dequeueBuffer(ANativeWindowBuffer** buffer, int* fenceFd);
    virtual int cancelBuffer(ANativeWindowBuffer* buffer, int fenceFd);
    virtual int queueBuffer(ANativeWindowBuffer* buffer, int fenceFd);
    ..............................;
    sp<IGraphicBufferProducer> mGraphicBufferProducer;
    ......................;

来翻译下这段代码中的注释:

creates a Surface from the given IGraphicBufferProducer (which concrete implementation is a BufferQueue).

Surface is mainly state-less while it’s disconnected, it can be viewed as a glorified IGraphicBufferProducer holder. It’s therefore safe to > create other Surfaces from the same IGraphicBufferProducer.

However, once a Surface is connected, it’ll prevent other Surfaces referring to the same IGraphicBufferProducer to become connected and therefore prevent them to be used as actual producers of buffers.

通过指定的IGraphicBufferProducer来创建一个Surface,而IGraphicBufferProducer是由具体的BufferQueue实现的。
当未连接时,Surface基本上是无状态的,可以把Surface看成是IGraphicBufferProducer光荣的持有者。因此此时可以使用相同的IGraphicBufferProducer去创建其他Surface。
当时一旦Surface已经连接,它将会阻止其他持有相同IGraphicBufferProducer的Surface编程连接状态,从而阻止他们被用作buffers的实际生产者。

从上面的注释来看,Surface需要一个IGraphicBufferProducer,而IGraphicBufferProducer由BufferQueue实现的。Surface在使用之前还需要connect。一堆问题随之而来:

  1. Surface一般是怎么创建的呢?
  2. BufferQueue是在哪里创建的呢?BufferQueue中的IGraphicBufferProducer给了Surface, 那Consumer是谁?
  3. Surface connect动作,是要connect谁呢 ?

surface创建

在网络上找到一个surface的demo:https://github.com/yrzroger/NativeSFDemo
将代码整理后,其主要代码逻辑如下:

//1. 创建surfaceComposerClient
sp<SurfaceComposerClient> surfaceComposerClient = new SurfaceComposerClient
//2. 通过surfaceComposerClient创建Surface
sp<SurfaceControl> surfaceControl = surfaceComposerClient->createSurface(mName, .......);
//3. 设置surface的状态
SurfaceComposerClient::Transaction{}
            .setLayer(surfaceControl, std::numeric_limits<int32_t>::max())
            .show(surfaceControl)
            .apply();
 sp<ANativeWindow> nativeWindow = mSurfaceControl->getSurface();
 //4. 连接surface
 native_window_api_connect(nativeWindow, NATIVE_WINDOW_API_CPU);
 ative_window_set_xxxxxx(.........);
 while(!quit) {
     //5. 获取一个input buffer
     nativeWindow->dequeueBuffer(nativeWindow, &nativeBuffer, &releaseFenceFd);
     sp<GraphicBuffer> buf(GraphicBuffer::from(nativeBuffer));
     // 6.写input buffer
     memcp(buf, .....);
     // 7.将写好的input buffer存入buffer queue
     nativeWindow->queueBuffer(nativeWindow, buf->getNativeBuffer(), acquireFenceFd);
 }

从上面的代码流程来看,Surface是包在surfaceComposerClient内部的。先看看surfaceComposerClient的代码。

sp<SurfaceControl> SurfaceComposerClient::createSurface(const String8& name, uint32_t w, uint32_t h,
                                                        PixelFormat format, uint32_t flags,
                                                        SurfaceControl* parent,
                                                        LayerMetadata metadata,
                                                        uint32_t* outTransformHint) {
    sp<SurfaceControl> s;
    createSurfaceChecked(name, w, h, format, &s, flags, parent, std::move(metadata),
                         outTransformHint);
    return s;
}
status_t SurfaceComposerClient::createSurfaceChecked(const String8& name, uint32_t w, uint32_t h,
                                                     PixelFormat format,
                                                     sp<SurfaceControl>* outSurface, uint32_t flags,
                                                     SurfaceControl* parent, LayerMetadata metadata,
                                                     uint32_t* outTransformHint) {
    sp<SurfaceControl> sur;
    status_t err = mStatus;
    if (mStatus == NO_ERROR) {
        sp<IBinder> handle;
        sp<IGraphicBufferProducer> gbp;
        uint32_t transformHint = 0;
        //下面这句代码,可以获得IGraphicBufferProducer,卧槽,这不就是创建Surface需要的么 ?
        // mClient是个什么玩意 ?后面再深入分析
        err = mClient->createSurface(name, w, h, format, flags, parentHandle, std::move(metadata),
                                     &handle, &gbp, &transformHint);
        if (outTransformHint) {
            *outTransformHint = transformHint;
        }
        if (err == NO_ERROR) {
            //创建一个SurfaceControl,看来Surface的创建在SurfaceControl中了。
            *outSurface = new SurfaceControl(this, handle, gbp, transformHint);
        }
    }
    return err;
}

SurfaceControl中创建Surface对象

sp<Surface> SurfaceControl::getSurface() const
{
    Mutex::Autolock _l(mLock);
    if (mSurfaceData == nullptr) {
        return generateSurfaceLocked();
    }
    return mSurfaceData;
}
//将mGraphicBufferProducer作为参数创建Surface
sp<Surface> SurfaceControl::generateSurfaceLocked() const
{
    // This surface is always consumed by SurfaceFlinger, so the
    // producerControlledByApp value doesn't matter; using false.
    mSurfaceData = new Surface(mGraphicBufferProducer, false);

    return mSurfaceData;
}

至此,Surface的创建已经找到地方了。那么继续找BufferQueue是在哪里创建的?
上面SurfaceComposerClient由这句代码可以获取IGraphicBufferProducer,那么BufferQueue肯定与此相关,那么来看看这个是什么。

mClient->createSurface(…)

sp<ISurfaceComposerClient>  mClient;
//实际上这玩意是一个Binder对象,是一个代理,其实体在surfaceflinger进程中。
void SurfaceComposerClient::onFirstRef() {
    //获取surfacecomposer的代理,其实体就是SurfaceFlinger这个类
    //class SurfaceFlinger : public BnSurfaceComposer,
    //                  public PriorityDumper,
    //                   public ClientCache::ErasedRecipient,
    //                   private IBinder::DeathRecipient,
    //                   private HWC2::ComposerCallback,
    //                   private ISchedulerCallback {
    //               .............;
    //        };
    // sf->createConnection() 实际上是执行sp<ISurfaceComposerClient> SurfaceFlinger::createConnection()
    // 这里涉及Android binder的知识,暂不深究。后面会出独立篇幅来研究Binder
    sp<ISurfaceComposer> sf(ComposerService::getComposerService());
    if (sf != nullptr && mStatus == NO_INIT) {
        sp<ISurfaceComposerClient> conn;
        //在surfaceflinger中创建SurfaceComposerClient的实体,返回SurfaceComposerClient的代理
        conn = sf->createConnection();
        if (conn != nullptr) {
            mClient = conn;
            mStatus = NO_ERROR;
        }
    }
}

sp<ISurfaceComposerClient> SurfaceFlinger::createConnection() {
    const sp<Client> client = new Client(this);
    return client->initCheck() == NO_ERROR ? client : nullptr;
}

上面代码已经解释SurfaceComposerClient中sp mClient为surfaceflinger中Client的代理。
那么mClient->createSurface(…)实际执行的是surfaceflinger的Client接口:Client::createSurface(…)
那么看看此接口中是如何创建IGraphicBufferProducer的?

status_t Client::createSurface(const String8& name, uint32_t w, uint32_t h, PixelFormat format,
                               uint32_t flags, const sp<IBinder>& parentHandle,
                               LayerMetadata metadata, sp<IBinder>* handle,
                               sp<IGraphicBufferProducer>* gbp, uint32_t* outTransformHint) {
    // We rely on createLayer to check permissions.
    return mFlinger->createLayer(name, this, w, h, format, flags, std::move(metadata), handle, gbp,
                                 parentHandle, nullptr, outTransformHint);
}

从下面的代码看是BufferQueueLayer中创建IGraphicBufferProducer

status_t SurfaceFlinger::createLayer(const String8& name, const sp<Client>& client, uint32_t w,
                                     uint32_t h, PixelFormat format, uint32_t flags,
                                     LayerMetadata metadata, sp<IBinder>* handle,
                                     sp<IGraphicBufferProducer>* gbp,
                                     const sp<IBinder>& parentHandle, const sp<Layer>& parentLayer,
                                     uint32_t* outTransformHint) {
   
    sp<Layer> layer;
    std::string uniqueName = getUniqueLayerName(name.string());
    bool primaryDisplayOnly = false;

    switch (flags & ISurfaceComposerClient::eFXSurfaceMask) {
        case ISurfaceComposerClient::eFXSurfaceBufferQueue:
            //一般普通的surface对应的是BufferQueueLayer
            result = createBufferQueueLayer(client, std::move(uniqueName), w, h, flags,
                                            std::move(metadata), format, handle, gbp, &layer);

            break;
        case ISurfaceComposerClient::eFXSurfaceBufferState:
            result = createBufferStateLayer(client, std::move(uniqueName), w, h, flags,
                                            std::move(metadata), handle, &layer);
            break;
        case ISurfaceComposerClient::eFXSurfaceEffect:
            result = createEffectLayer(client, std::move(uniqueName), w, h, flags,
                                       std::move(metadata), handle, &layer);
            break;
        case ISurfaceComposerClient::eFXSurfaceContainer:
            result = createContainerLayer(client, std::move(uniqueName), w, h, flags,
                                          std::move(metadata), handle, &layer);
            break;
       ...........;
    }
    result = addClientLayer(client, *handle, *gbp, layer, parentHandle, parentLayer,
                            addToCurrentState, outTransformHint);
    .........................;
    return result;
}

继续看下IGraphicBufferProducer的代码:

status_t SurfaceFlinger::createBufferQueueLayer(const sp<Client>& client, std::string name,
                                                uint32_t w, uint32_t h, uint32_t flags,
                                                LayerMetadata metadata, PixelFormat& format,
                                                sp<IBinder>* handle,
                                                sp<IGraphicBufferProducer>* gbp,
                                                sp<Layer>* outLayer) {
    .........................................;
    sp<BufferQueueLayer> layer;
    LayerCreationArgs args(this, client, std::move(name), w, h, flags, std::move(metadata));
    args.textureName = getNewTexture();
    {
        Mutex::Autolock lock(mStateLock);
        //创建BufferQueueLayer
        layer = getFactory().createBufferQueueLayer(args);
    }
    status_t err = layer->setDefaultBufferProperties(w, h, format);
    if (err == NO_ERROR) {
        *handle = layer->getHandle();
        //获取IGraphicBufferProducer
        *gbp = layer->getProducer();
        *outLayer = layer;
    }
    return err;
}

sp<BufferQueueLayer> DefaultFactory::createBufferQueueLayer(const LayerCreationArgs& args) {
    return new BufferQueueLayer(args);
}

layer的继承关系:

class BufferQueueLayer : public BufferLayer {
    .........;
}
class BufferLayer : public Layer {
    .........;
}

class Layer : public virtual RefBase, compositionengine::LayerFE {
    ............;
}

BufferQueueLayer在哪里创建BufferQueue的呢 ?

void BufferQueueLayer::onFirstRef() {
    BufferLayer::onFirstRef();
    // Creates a custom BufferQueue for SurfaceFlingerConsumer to use
    sp<IGraphicBufferProducer> producer;
    sp<IGraphicBufferConsumer> consumer;
    //创建BufferQueue
    mFlinger->getFactory().createBufferQueue(&producer, &consumer, true);
    mProducer = mFlinger->getFactory().createMonitoredProducer(producer, mFlinger, this);
    mConsumer =
            mFlinger->getFactory().createBufferLayerConsumer(consumer, mFlinger->getRenderEngine(),
    ................................;
}

void DefaultFactory::createBufferQueue(sp<IGraphicBufferProducer>* outProducer,
                                       sp<IGraphicBufferConsumer>* outConsumer,
                                       bool consumerIsSurfaceFlinger) {
    BufferQueue::createBufferQueue(outProducer, outConsumer, consumerIsSurfaceFlinger);
}

void BufferQueue::createBufferQueue(sp<IGraphicBufferProducer>* outProducer,
        sp<IGraphicBufferConsumer>* outConsumer,
        bool consumerIsSurfaceFlinger) {
    sp<BufferQueueCore> core(new BufferQueueCore());
    sp<IGraphicBufferProducer> producer(new BufferQueueProducer(core, consumerIsSurfaceFlinger));
    sp<IGraphicBufferConsumer> consumer(new BufferQueueConsumer(core));
    *outProducer = producer;
    *outConsumer = consumer;
}

所以用于创建Surface的IGraphicBufferProducer实际为BufferQueueProducer, 而IGraphicBufferConsumer实际为BufferQueueConsumer储存在BufferQueueLayer中。

surface connect

native_window_api_connect(nativeWindow, NATIVE_WINDOW_API_CPU);
这个c接口最终是调用下面Surface::connect(…)

int Surface::connect(int api, const sp<IProducerListener>& listener, bool reportBufferRemoval) {
     ...................;
    int err = mGraphicBufferProducer->connect(listener, api, mProducerControlledByApp, &output);
    ....................;
    return err;
}

status_t BufferQueueProducer::connect(const sp<IProducerListener>& listener,
        int api, bool producerControlledByApp, QueueBufferOutput *output) {
    //如果已经连接过,就不允许再链接。也就是BufferQueueCore只允许由一个connected producer
    if (mCore->mConnectedApi != BufferQueueCore::NO_CONNECTED_API) {
        BQ_LOGE("connect: already connected (cur=%d req=%d)",
                mCore->mConnectedApi, api);
        return BAD_VALUE;
    }
    ..........................;
    int status = NO_ERROR;
    switch (api) {
        case NATIVE_WINDOW_API_EGL:
        case NATIVE_WINDOW_API_CPU:
        case NATIVE_WINDOW_API_MEDIA:
        case NATIVE_WINDOW_API_CAMERA:
            mCore->mConnectedApi = api;
            output->width = mCore->mDefaultWidth;
            output->height = mCore->mDefaultHeight;
            output->transformHint = mCore->mTransformHintInUse = mCore->mTransformHint;
            output->numPendingBuffers =
                    static_cast<uint32_t>(mCore->mQueue.size());
            output->nextFrameNumber = mCore->mFrameCounter + 1;
            output->bufferReplaced = false;
            output->maxBufferCount = mCore->mMaxBufferCount;
          ................................;
    }
    ..........................................;
    return status;
}

从上面的代码逻辑来看,surface connect的对象是surface中的producer对应的BufferQueueCore,BufferQueueCore同一时间只能链接一个producer因此可以阻止多次链接的问题。另外在connect的时候会获取BufferQueueCore的一些信息。

画图总结

问题解答

  1. 什么是surface ? surface与图形数据之间是什么关系?
    surface是bufferqueue中bufferproducer的一个holder, 一个surface对应surfaceflinger中的一个layer, layer作为bufferqueue中buffer consumer的holder。app的UI/视频/camera图形数据通过surface的bufferproducer将数据发送到surfaceflinger中对应layer中的bufferproducer中,最终由layer中的buffer consumer取出图形数据进行合成显示。
    surface是图形数据传输到surfaceflinger的一个通道。
  2. surface和surfaceflinger之间是什么关系?
    一个surface与surfaceflinger中一个layer一一对应。layer中有bufferqueue用于接收surface通过producer发过来的图形数据。

关系图surface

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/353050.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

k8s(存储)数据卷与数据持久卷

为什么需要数据卷&#xff1f; 容器中的文件在磁盘上是临时存放的&#xff0c;这给容器中运行比较重要的应用程序带来一些问题问题1&#xff1a;当容器升级或者崩溃时&#xff0c;kubelet会重建容器&#xff0c;容器内文件会丢失问题2&#xff1a;一个Pod中运行多个容器并需要共…

创邻科技荣获人行旗下《金融电子化》年度大奖

近日&#xff0c;创邻科技收到由中国人民银行旗下《金融电子化》杂志社寄来的奖牌。 在《金融电子化》杂志社主办的第十三届金融科技应用创新奖中&#xff0c;创邻科技凭借“原生分布式图数据库Galaxybase解决方案”&#xff0c;从近400个参报案例中脱颖而出&#xff0c;荣获“…

Linux下zabbix_proxy实施部署

简介 zabbix proxy 可以代替 zabbix server 收集性能和可用性数据,然后把数据汇报给 zabbix server,并且在一定程度上分担了zabbix server 的压力. zabbix-agent可以指向多个proxy或者server zabbix-proxy不能指向多个server zabbix proxy 使用场景: 1&#xff0c;监控远程区…

【React全家桶】reac组件通信

&#x1f39e;️&#x1f39e;️&#x1f39e;️ 博主主页&#xff1a; 糖 &#xff0d;O&#xff0d; &#x1f449;&#x1f449;&#x1f449; react专栏&#xff1a;react全家桶 &#x1f339;&#x1f339;&#x1f339;希望各位博主多多支持&#xff01;&#xff01;&a…

自媒体市场规模由2015年的296亿元增涨至2021年的2500亿元

自媒体&#xff0c;又称“个人媒体”&#xff0c;是指大众化、自主化的传播者以图文、音频或视频内容等各类形式向公众发布信息内容。随着&#xff15;&#xff27;时代的来临和智能设备的性能逐渐提高&#xff0c;网络基础环境得到很大的提升&#xff0c;自媒体开始了新的发展…

QT的下载和安装

这里介绍的是QT官方方式下载&#xff0c;每次都让我很糊涂&#xff0c;就记载一下。先是下载QT online installerhttps://www.qt.io/download 在下方有Go Open Sourcehttps://www.qt.io/download-open-source 在下方有Download the Qt Online installerhttps://www.qt.io/downl…

C#(NET Core3.1 MVC)生成站点地图(sitemap.xml)

要做SEO的肯定绕不开站点地图sitemap.xml。这玩意其实不难我也在搞写下来备忘一下也给新人指指路。 我先把代码放出来备忘下 #region CreateSiteMapXml/// <summary>/// /// </summary>/// <returns></returns>[Route("/art/CreateSiteMapXml&qu…

Windows下安装启动nginx.exe报错

Windows下安装启动nginx.exe报错 前言&#xff1a; 问题1&#xff1a; 在安装使用nginx服务器时遇到最大的问题是windows下命令行输入start nginx后&#xff0c;或者双击nginx.exe&#xff0c;一闪而过&#xff0c;启动不了&#xff0c;怀疑是以下几个方面的问题&#xff0c;…

高效率工作之关于进入心流的方法

本文是向大家介绍高效率工作体验——心流&#xff01;心流也叫最优体验&#xff0c;是一位叫米哈里的心理学家在调查研究的基础上提出的概念。心流指的是一种将大脑注意力毫不费力地集中起来的状态&#xff0c;这种状态使人忘记时间的概念&#xff0c;忘掉自我&#xff0c;也忘…

商家必读!超店有数分享,tiktok达人营销变现如何更快一步?

近几年来&#xff0c;“粉丝经济”发展越来越迅猛&#xff0c;“网红带货”已经成为了一种营销的方式。这种方式让商家能基于达人的影响下迅速抢占自己的私域流量池。消费者会基于对达人的信任&#xff0c;购买达人推荐的产品。达人效应可以助力品牌走出营销困境。如果商家想要…

cmake 引入第三方库(头文件目录、库目录、库文件)

程序的编写需要用到头文件&#xff0c;程序的编译需要lib文件&#xff0c;程序的运行需要dll文件&#xff0c;因此cmake引入第三方库其实就是将include目录、lib目录、bin目录引入工程。 目录 1、find_package&#xff08;批量引入库文件和头文件&#xff09; 2、include_dir…

什么是禅道?禅道可以做什么?如何自动推送禅道消息?

什么是禅道&#xff1f;禅道是一款国产的开源项目管理软件。它的核心管理思想基于敏捷方法 scrum&#xff0c;内置了产品管理和项目管理&#xff0c;同时又根据国内研发现状补充了测试管理、计划管理、发布管理、文档管理、事务管理等功能&#xff0c;在一个软件中就可以将软件…

北京地铁口免费的大白鹅,你领了吗?

最近这几天&#xff0c;北京很多个地铁口周围涌现了许多只又白又肥的大鹅。 不过&#xff0c;此大鹅非铁锅之中的炖大鹅&#xff0c;乃是又呆又萌的大鹅玩偶。 而且&#xff0c;还是免费送的&#xff01; 所以每天晚上的下班时间&#xff0c;在一群掐着细长的大鹅脖子的地推人…

02:入门篇 - 漫谈 CTK

作者: 一去、二三里 个人微信号: iwaleon 微信公众号: 高效程序员 十万个为什么 五千个在哪里?七千个怎么办?十万个为什么?。。。生活中,有很多奥秘在等着我们去思考、揭示! 同样地,在使用 CTK 时,很多小伙伴一定也存在诸多疑问: 为什么 CTK Plugin Framework 要借…

计算机中有符号数的表示

文章目录二进制数制十进制二进制位模式基本数据类型无符号数的编码有符号数的编码原码&#xff08;Sign-Magnitude&#xff09;反码&#xff08;Ones Complement&#xff09;补码&#xff08;Twos Complement&#xff09;概念导读编码格式按权展开补码加法扩展一个数字的位表示…

基于STM32L431+Liteos的串口空闲中断加DMA循环接收

①MCU为STM32L431&#xff0c;使用串口2。 ②Liteos采用接管中断的方式。 STM32CubeMX配置生成串口代码&#xff1a; 串口DMA接收和发送配置区别是接收采用循环模式&#xff0c;发送为正常模式。 将生成的代码移植到liteos工程中&#xff0c;由于使用的接管中断的方式&#…

appium桌面版本以及一些自动化测试方方封装

标签&#xff08;空格分隔&#xff09;&#xff1a; appium_desktop 一 appium_desktop_v1.2.6 1.appium_desktop在github上最新下载地址&#xff1a;appium桌面版本地址 2.一路傻瓜式安装就好了&#xff1a; 3.然后点击搜索按钮&#xff08;右上角&#xff09; 三 inspector …

基于国产龙芯 CPU 的气井工业网关研究与设计(二)

3.1 系统硬件的总体设计 从硬件架构上&#xff0c;该 RTU 主要包括三大部分的设计&#xff1a; &#xff08;1&#xff09;外围电路设计&#xff1a;电源电路设计、RTC 电路设计和 EEPROM 电路设计。 &#xff08;2&#xff09;RTU 本体 I/O 端口设计&#xff1a;A/I 模拟量输入…

使用RedisDesktopManager无法连接Redis服务器问题

问题&#xff1a;解决办法问题1&#xff1a;redis的配置文件问题进入redis的目录 [rootredis ~]# cd /opt/apps/redis/ opt apps 是自己创建的文件夹&#xff08;用于安装redis&#xff09; 使用vim进入配置文件的修改 [rootredis redis]# vim redis.conf使用vim编辑器修改bi…

ERROR 1114 (HY000): The table ‘tt2‘ is full

insert 操作时提示is full 问题原因 rootlocalhost 11:55:41 [t]>show table status from t like ‘tt2’ \G ; *************************** 1. row *************************** Name: tt2 Engine: MEMORY Version: 10 Row_format: Fixed Rows: 7056 Avg_row_length: 944…