Pag的2D渲染执行流程

文章详细阐述了Pag渲染引擎的工作流程,包括从使用Skia到自研TGFX库的转变,以及渲染过程中的关键步骤,如设置进度、组织渲染指令、执行渲染和上屏渲染。在渲染流程中,重点分析了如何通过内容版本控制优化性能,以及如何通过TGFX在GPU上构建和执行渲染指令。最后,提到了Pag如何通过Drawable的present方法将渲染结果上屏。
摘要由CSDN通过智能技术生成

Pag的渲染

背景

根据Pag文章里面说的,Pag之前长时间使用的Skia库作为底层渲染引擎。但由于Skia库体积过大,为了保证通用型(比如兼容CPU渲染)做了很多额外的事情。所以Pag的工程师们自己实现了一套2D图形框架替换掉Skia,在github里面在一个叫 TGFX 的目录内。我猜测腾讯的工程师这里是致敬暴雪的bgfx吧,不过这两个库其实有本质区别:bgfx更多是统一底层图形API为目标的,而tgfx是一套用于2D的渲染库,核心目标是替换掉Skia。其目前主要用OpenGL API用GPU实现渲染,默认不支持CPU渲染(也能通过SwiftShader来支持),在绝大部分设备上GPU渲染都会更高效。其API也和Skia的API相似度很高,只是没有SK开头罢了。这些API对于做Android应用开发的同学来说再熟悉不过了。不过我还是整体理一下里面的核心概念吧:

概念描述
Bitmap位图,它是像素的集合,是内存里的色彩的表现和承载者,一般用CPU解码图片后就会保存为Bitmap,也可以通过CPU去操作Bitmap的像素。
Texture纹理,也代表一张图,在OpenGL内使用,可以简单的理解为在显存内的Bitmap,Pag里面的视频/素材 都是通过Texture渲染的。
Surface表面,其实更像是一个装画的地方。画画我们可以在纸上画,也可以在墙上画。
Paint画笔,可以自定义各种色彩,样式等,拿起什么样的比就画出什么样的画。
Canvas画布,可以看做一种渲染过程,比如draw/drawTexture/drawPath等。经历过怎样的渲染过程就会产出什么样的结果。比如我画了一个狮子,再画一个老虎,那最终就会呈现出虎狮

概要

为了了解全貌,我这边把一些重要的概念和整体的Pag模块画了一个图,大家可以看到TGFX在整个libpag内的位置:
Pag整体架构

接下来我们还是从情景来分析pag渲染的具体流程。

Pag渲染流程分析

之前我们介绍了Pag的基本结构组织,这里来详细讲一下渲染过程,也就是如下两个方法调用:

1. 设置进度
pagPlayer.setProgress(progress);
2. 执行渲染
pagPlayer.flush();

设置进度

这个应该都大致能猜到,肯定核心逻辑就是会去设置到目标合成里通过归一化的percent通知到RootComposition当前有进度更新,需要准备下一次渲染的素材了之类的逻辑。具体代码如下:

void PAGPlayer::setProgress(double percent) {
  LockGuard autoLock(rootLocker);
  auto pagComposition = stage->getRootComposition();
  if (pagComposition == nullptr) {
    return;
  }
  auto realProgress = percent;
  auto frameRate = pagComposition->frameRateInternal();
  if (_maxFrameRate < frameRate && _maxFrameRate > 0) {
    auto duration = pagComposition->durationInternal();
    auto totalFrames = TimeToFrame(duration, frameRate);
    auto numFrames = ceilf(totalFrames * _maxFrameRate / frameRate);
    // 首先计算在maxFrameRate的帧号,之后重新计算progress
    auto targetFrame = ProgressToFrame(realProgress, numFrames);
    realProgress = FrameToProgress(targetFrame, numFrames);
  }
  pagComposition->setProgressInternal(realProgress);
}

发现这里其实还有一个播放器的最大渲染帧率的转换逻辑,如果合成的帧率比PagPlayer的目标帧率还高,需要根据间隔去做采样为播放器的最大帧率。这个在低端设备上应该会对性能会有一些提升,能够更大程度的利用缓存。核心的pagComposition->setProgressInternal(realProgress);是实现在PagLayer内的,它会通过progress计算出当前应该播放的时间,然后调用gotoTimeAndNotifyChanged方法:

bool PAGLayer::gotoTimeAndNotifyChanged(int64_t targetTime) {
  auto changed = gotoTime(targetTime);
  if (changed) {
    notifyModified();
  }
  return changed;
}

顾名思义,先Seek到指定的时间,然后如果发现图层需要更新(核心是一个_stretchedFrameDuration,这个是在AE插件里面保存的值,如果是静态图之类的,会和duration相同,更新progress不需要改变内容),则会调用notifyModified去通知有更新,我们看看里面的实现:

void PAGLayer::notifyModified(bool contentChanged) {
  if (contentChanged) {
    contentVersion++;
  }
  auto parentLayer = getParentOrOwner();
  while (parentLayer) {
    parentLayer->contentVersion++;
    parentLayer = parentLayer->getParentOrOwner();
  }
}

可以看到首先把自己的contentVersion做了自增,然后如果有父图层以及父图层更新contentVersion,我们这里本身是根图层更新,不会涉及parent。下一次渲染的时候会对比这个contentVersion和上一次的是否一致,如果一致的话,就不需要重新绘制了,通过这个version来避免额外性能损耗。

渲染过程

渲染过程的流程主要是分为:

  1. 组织渲染 – 主要是通过调用Canvas的一些方法,把所有渲染相关的指令组织起来,保存到上下文内
  2. 执行渲染 – 把上面组织起来的所有渲染相关的指令flush到GPU内,完成一次渲染
  3. 上屏渲染 – 等待渲染操作完成,把渲染完成的Buffer上屏到显示设备上

我这里先画一个简单的类图,把里面涉及到的大致的概念呈现出来,让大家有一个简单的认识。把里面的一些主要概念的关系画出来,也包含了里面的一些功能说明。
渲染相关类图

组织渲染

Pag执行渲染的方法叫做flush,就是把当前设置的这些配置要给执行了。Flush的实现核心是调用了flushInternal方法:

bool PAGPlayer::flushInternal(BackendSemaphore* signalSemaphore) {
  ...
  prepareInternal();
  clock.mark("rendering");
  if (!pagSurface->draw(renderCache, lastGraphic, signalSemaphore, _autoClear)) {
    return false;
  }
  ...
  return true;
}

我们这里省略了大部分其他代码,直接看了两个最重要的prepareInternalpagSurface->draw()调用就可以了。
prepareInternal的实现如下:

void PAGPlayer::prepareInternal() {
  // 为了提升性能,预加载加载视频和图片
  renderCache->prepareLayers();
  // 通过contentVersion来判断stage是否有刷新,没有刷新的话就不用去重新渲染了
  if (contentVersion != stage->getContentVersion()) {
    // 更新当前的渲染内容版本号
    contentVersion = stage->getContentVersion();
    Recorder recorder = {};
    // 难道在这里就渲染了?并不是哦,只是组织图层到recorder里面
    stage->draw(&recorder);
    lastGraphic = recorder.makeGraphic();
  }
  if (lastGraphic) {
    lastGraphic->prepare(renderCache);
  }
}

可以看到这里在判断需要刷新stage的时候有个stage->draw(&recorder);调用,stage代码如下:

void PAGComposition::draw(Recorder* recorder) {
  ... // 这里省略缓存策略
  auto preComposeLayer = static_cast<PreComposeLayer*>(layer);
  auto composition = preComposeLayer->composition;
  ... // 这里省略位图或者视频策略,Clip和判空逻辑
  auto count = static_cast<int>(layers.size());
  // 遍历所有的图层,然后挨个调用DrawChildLayer
  for (int i = 0; i < count; i++) {
    auto& childLayer = layers[i];
    if (!childLayer->layerVisible) {
      continue;
    }
    DrawChildLayer(recorder, childLayer.get());
  }
  ... // 省略Clip逻辑
}

这个方法核心其实就是遍历所有的childLayer,然后挨个调用DrawChildLayer把每一层存入Recorder内,我们看看DrawChildLayer的代码:

void PAGComposition::DrawChildLayer(Recorder* recorder, PAGLayer* childLayer) {
  // 图层的特效Modifier
  auto filterModifier = childLayer->cacheFilters() ? nullptr : FilterModifier::Make(childLayer);
  // 多点追踪使用的,暂时可以不用理会
  auto trackMatte = TrackMatteRenderer::Make(childLayer);
  Transform extraTransform = {ToTGFX(childLayer->layerMatrix), childLayer->layerAlpha};

  LayerRenderer::DrawLayer(recorder, childLayer->layer,
                           childLayer->contentFrame + childLayer->layer->startTime, filterModifier,
                           trackMatte.get(), childLayer, &extraTransform);
}

看到这个代码有点晕了,开始有TGFX的影子了。这里核心是调用静态函数LayerRenderer::DrawLayer:

void LayerRenderer::DrawLayer(Recorder* recorder, Layer* layer, Frame layerFrame,
                              std::shared_ptr<FilterModifier> filterModifier,
                              TrackMatte* trackMatte, Content* layerContent,
                              Transform* extraTransform) {
  if (TransformIllegal(extraTransform) || TrackMatteIsEmpty(trackMatte)) {
    return;
  }
  auto contentFrame = layerFrame - layer->startTime;
  // 这里比较核心返回一个layoutCache,里面有包含各种图层类型的渲染内容缓存的实现。
  auto layerCache = LayerCache::Get(layer);
  if (!layerCache->contentVisible(contentFrame)) {
    return;
  }
  auto content = layerContent ? layerContent : layerCache->getContent(contentFrame);
  ... //省略alpha,Blend, 多点追踪逻辑
  auto saveCount = recorder->getSaveCount();
  ... // 省略其他Transferm,Mask等逻辑
  // 核心draw逻辑
  content->draw(recorder);
  recorder->restoreToCount(saveCount);
  ... // 省略多点追踪逻辑
  recorder->restore();
}

可以看到这里核心流程就是通过 auto layerCache = LayerCache::Get(layer); 创建/获取了一个LayerCache。然后执行LayerCache的content的draw方法到recorder对象内。看看LayerCache::Get的代码:

LayerCache* LayerCache::Get(Layer* layer) {
  std::lock_guard<std::mutex> autoLock(layer->locker);
  if (layer->cache == nullptr) {
    layer->cache = new LayerCache(layer);
  }
  return static_cast<LayerCache*>(layer->cache);
}
LayerCache::LayerCache(Layer* layer) : layer(layer) {
  switch (layer->type()) {
    case LayerType::Shape:
      contentCache = new ShapeContentCache(static_cast<ShapeLayer*>(layer));
      break;
    case LayerType::Text:
      contentCache = new TextContentCache(static_cast<TextLayer*>(layer));
      break;
    case LayerType::Solid:
      contentCache = new SolidContentCache(static_cast<SolidLayer*>(layer));
      break;
    case LayerType::Image:
      contentCache = new ImageContentCache(static_cast<ImageLayer*>(layer));
      break;
    case LayerType::PreCompose:
      contentCache = new PreComposeContentCache(static_cast<PreComposeLayer*>(layer));
      break;
    default:
      contentCache = new EmptyContentCache(layer);
      break;
  }
  contentCache->update();
  transformCache = new TransformCache(layer);
  if (!layer->masks.empty()) {
    maskCache = new MaskCache(layer);
  }
  updateStaticTimeRanges();
  maxScaleFactor = ToTGFX(layer->getMaxScaleFactor());
}

其实核心就是通过ContentCache类型分别创建GraphicContent类型。这个GraphicContent是一个用于具体实现不同类型的图形的结构了。有一点像Android里面的View。接下来调用了核心的content.draw(recorder)把需要渲染的GraphicsContent存入到我们的Recorder内。

void Recorder::drawGraphic(std::shared_ptr<Graphic> graphic) {
  auto content = Graphic::MakeCompose(std::move(graphic), matrix);
  if (content == nullptr) {
    return;
  }
  if (layerIndex == 0) {
    rootContents.push_back(content);
  } else {
    layerContents.push_back(content);
  }
}

从这里也说明了这些函数虽然叫做drawXXX但是在这一步并没有真正的执行任何渲染相关的指令。
接下来回到一开始的prepareInternal,我们已经分析了这里通过这个Recorder保存了渲染需要用的content。prepareInternal最后做的事情就是通过这个recorder再生成一个具体用于渲染的Graphic对象,并且再prepare这个Graphic:

  lastGraphic = recorder.makeGraphic();
  if (lastGraphic) {
    lastGraphic->prepare(renderCache);
  }

这个lastGraphic是一个LayerGraphic,而下一步lastGraphic->prepare才真正的是prepare要渲染的内容的。LayerGraphic是一个树形结构的包装对象,和Android里面的ViewGroup十分类似。其prepare也就是深度遍历这个渲染树,把所有的节点都prepare一遍。

void LayerGraphic::prepare(RenderCache* cache) const {
  for (auto& content : contents) {
    content->prepare(cache);
  }
}

针对不同的Content,会有不同的prepare过程。比如对图片的内容来说,就可以在这里去获取解码好的图片,如果没有解码好需要等待解码完成,视频也需要在这里等这个时间的帧解码好才能去做渲染。

渲染核心过程

组织渲染指令

准备工作看完了,我们这里开始来深入分析一下这个pagSurface->draw。先上代码,这里我加上了注释:

bool PAGSurface::draw(RenderCache* cache, std::shared_ptr<Graphic> graphic,
                      BackendSemaphore* signalSemaphore, bool autoClear) {
  // 之前提到过,这里的drawable代表的是一个抽象的GPU渲染实例(GPUDrawable/OffscreenDrawable等),并不是做具体绘制的实例。
  if (!drawable->prepareDevice()) {
    return false;
  }
  // 获取用于渲染的上下文,这里拿到的是GLContext
  auto context = lockContext();
  if (!context) {
    return false;
  }
  if (surface != nullptr && autoClear && contentVersion == cache->getContentVersion()) {
    unlockContext();
    return false;
  }
  // 如果Surface为空,则创建。这里的Surface是指的TGFX内的
  if (surface == nullptr) {
    surface = drawable->createSurface(context);
  }
  if (surface == nullptr) {
    unlockContext();
    return false;
  }
  contentVersion = cache->getContentVersion();
  cache->attachToContext(context);
  auto canvas = surface->getCanvas();
  if (autoClear) {
    canvas->clear();
  }
  if (graphic) {
    // 核心渲染的操作,会生成draw call
    graphic->draw(canvas, cache);
  }
  // 这里具体去执行draw call,把CPU内组合的渲染操作(Op最终同步到GPU内
  if (signalSemaphore == nullptr) {
    // 同步模式下直接flush操作
    surface->flush();
  } else {
    // 这里多了异步渲染模式下的同步锁操作。
    tgfx::GLSemaphore semaphore = {};
    surface->flush(&semaphore);
    signalSemaphore->initGL(semaphore.glSync);
  }
  cache->detachFromContext();
  drawable->setTimeStamp(pagPlayer->getTimeStampInternal());
  drawable->present(context);
  unlockContext();
  return true;
}

上面最核心的是两个调用。一个是生成DrawCall的

  if (graphic) {
    graphic->draw(canvas, cache);
  }

最终会把在prepareInternal里面生成的所有Content调用draw来生成这个content当前状态对应的OpenGL渲染指令集。我们这里用绘制一个Shape来举例:

void Shape::draw(tgfx::Canvas* canvas, RenderCache* renderCache) const {
  tgfx::Paint paint;
  auto snapshot = renderCache->getSnapshot(this);
  if (snapshot) {
    ...
    // 这里省略了使用缓存的逻辑 
    ...
  }
  paint.setShader(shader);
  canvas->drawPath(path, paint);
}

是不是和我们在Android里面的drawPath很像?这里的canvas的实现是GLCanvas:

void GLCanvas::fillPath(const Path& path, const Paint& paint) {
  ...
  // 注意这里,创建了一个GLDrawOp
  auto op = MakeSimplePathOp(path, glPaint, state->matrix);
  if (op) {
    draw(std::move(op), std::move(glPaint));
    return;
  }
  ...
  op = GLTriangulatingPathOp::Make(glPaint.color, tempPath, state->clip.getBounds(), localMatrix);
  if (op) {
    save();
    resetMatrix();
    draw(std::move(op), std::move(glPaint));
    restore();
    return;
  }
  ...
  drawMask(deviceBounds, mask->makeTexture(getContext()), std::move(glPaint));
}

我们这里省略了很多代码,基本可以看到流程就是创建GLDrawOp,在里面设置一堆参数,最后调用draw或者drawMask(最终也是调用到了draw),我们来看看这个draw方法:

void GLCanvas::draw(std::unique_ptr<GLDrawOp> op, GLPaint paint, bool aa) {
  if (drawContext == nullptr) {
    return;
  }
  // 设置抗锯齿类型
  auto aaType = AAType::None;
  if (static_cast<GLSurface*>(surface)->renderTarget->sampleCount() > 1) {
    aaType = AAType::MSAA;
  } else if (aa && !IsPixelAligned(op->bounds())) {
    aaType = AAType::Coverage;
  } else {
    const auto& matrix = state->matrix;
    auto rotation = std::round(RadiansToDegrees(atan2f(matrix.getSkewX(), matrix.getScaleX())));
    if (static_cast<int>(rotation) % 90 != 0) {
      aaType = AAType::Coverage;
    }
  }
  // 设置Mask
  auto masks = std::move(paint.coverageFragmentProcessors);
  Rect scissorRect = Rect::MakeEmpty();
  auto clipMask = getClipMask(op->bounds(), &scissorRect);
  if (clipMask) {
    masks.push_back(std::move(clipMask));
  }
  // 设置渲染区域
  op->setScissorRect(scissorRect);
  // 设置Blend图层叠加模式
  unsigned first;
  unsigned second;
  if (BlendAsCoeff(state->blendMode, &first, &second)) {
    op->setBlendFactors(std::make_pair(first, second));
  } else {
    op->setXferProcessor(PorterDuffXferProcessor::Make(state->blendMode));
    op->setRequireDstTexture(!GLCaps::Get(getContext())->frameBufferFetchSupport);
  }
  op->setAA(aaType);
  // 配置颜色
  op->setColors(std::move(paint.colorFragmentProcessors));
  op->setMasks(std::move(masks));
  // 添加到渲染队列
  drawContext->addOp(std::move(op));
}

基本的核心配置我已经在代码里给了注释。这个地方特别重要,属于渲染的核心流程,所以代码我没有省略。
这里设置了一些Paint的通用配置,然后加入到绘制的上下文drawContext->addOp(std::move(op));

void SurfaceDrawContext::addOp(std::unique_ptr<Op> op) {
  getOpsTask()->addOp(std::move(op));
}

很简单,其实就是把当前这一个Content的Op添加到opsTask列表里面了。说明这里其实还没有具体去执行这些OpenGL指令。值得注意的是这个getOpsTask里面有一个很重要的操作:

OpsTask* SurfaceDrawContext::getOpsTask() {
  if (opsTask == nullptr || opsTask->isClosed()) {
    replaceOpsTask();
  }
  return opsTask.get();
}

void SurfaceDrawContext::replaceOpsTask() {
  opsTask = surface->getContext()->drawingManager()->newOpsTask(surface);
}

这个opsTask是通过replaceOpsTask里面的surface->getContext()->drawingManager()->newOpsTask(surface);创建的。这样就会被DrawingManager所管理起来,之后再被统一执行。

执行渲染

上面基本就把准备渲染的opTask准备好了。其实渲染的准备工作是最重要的,执行渲染只是把前面存放的即将用于渲染的指令具体去执行罢了。

if (signalSemaphore == nullptr) {
    surface->flush();
  } else {
    tgfx::GLSemaphore semaphore = {};
    surface->flush(&semaphore);
    signalSemaphore->initGL(semaphore.glSync);
  }

很明显这里面的这个Surface->flush就是渲染具体执行的地方了。里面的代码很简单:

bool Surface::flush(Semaphore* signalSemaphore) {
  // 解析渲染的目标。因为Pag支持设置渲染到纹理上,不一定都是上屏,所以这里会手动插入一个配置Target的Task
  renderTarget->getContext()->drawingManager()->newTextureResolveRenderTask(this);
  // 这里把之前保存的opTask都执行了
  return renderTarget->getContext()->drawingManager()->flush(signalSemaphore);
}

我们看看这里核心调用的DrawingManager->flush具体是做哪些事:

bool DrawingManager::flush(Semaphore* signalSemaphore) {
  auto* gpu = context->gpu();
  closeAllTasks();
  activeOpsTask = nullptr;
  for (auto& task : tasks) {
    task->execute(gpu);
  }
  removeAllTasks();
  return context->caps()->semaphoreSupport && gpu->insertSemaphore(signalSemaphore);
}

可以看到这里的核心就是会把之前所有的GLCanvas执行的opTask都执行一遍,然后再清空这个tasks列表。这样就完成了渲染指令的具体执行了。

上屏渲染

在PagSurface的draw最后几段代码如下:

  drawable->setTimeStamp(pagPlayer->getTimeStampInternal());
  drawable->present(context);

一开始的时候提到过,Pag里面的Drawable其实是一个渲染设备的封装。这个present在最后会调用到OpenGL的swapbuffer函数,将前面的所有操作交换缓冲区实现上屏到具体的渲染Target。这里我们就不具体去分析了。

总结

今天这个文章里面的代码比较多了,有一点乱,而且这里只是一次渲染,还没有讲到Pag如何管理渲染的缓存,实现上一期讲的纵向分层,水平分块把性能提升的。如果要深入理解建议自己去Debug到Pag的流程里面才能真正的了解内部渲染的执行过程,能够学到更多的东西。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值