当前位置: 首页 > 工具软件 > RenderDoc > 使用案例 >

Renderdoc源码研究——duration计算过程

长孙阳焱
2023-12-01

源码逻辑

1、点击面板上的图标,触发事件 on_timeDraws_clicked()

GPUCounter 这个结构体中会存储很多变量,其中的EventGPUDuration 变量就是存放的duration的值。

【定义】GPUCounter:: EventGPUDuration —— 用于表示:此事件在 GPU 上花费的时间,由两个 GPU 时间戳之间的增量衡量。

// EventBrowser.cpp
void EventBrowser::on_timeDraws_clicked()
{
  ANALYTIC_SET(UIFeatures.DrawcallTimes, true);

  ui->events->header()->showSection(COL_DURATION);

  m_Ctx.Replay().AsyncInvoke([this](IReplayController *r) {

    // 调用FetchCounters函数来获取Duration
    m_Times = r->FetchCounters({GPUCounter::EventGPUDuration});

    GUIInvoke::call(this, [this]() {
      if(ui->events->topLevelItemCount() == 0)
        return;

      // 根据m_Times中存放的结果,以及该时间对应的event 分别设置其变量。树状结构,子节点的时间和为父节点的值。
      SetDrawcallTimes(ui->events->topLevelItem(0), m_Times);
      ui->events->update();
    });
  });
}

2、深入查看如何计算出m_Times的值。跳转到文件 replay_controller.cpp 中的FetchCounters

rdcarray<CounterResult> ReplayController::FetchCounters(const rdcarray<GPUCounter> &counters)
{
  CHECK_REPLAY_THREAD();

  RENDERDOC_PROFILEFUNCTION();

    // 根据当前获取到的设备信息和状态,来调用不同的函数FetchCounters
    // 此处用到了虚函数,动态多态。
  return m_pDevice->FetchCounters(counters);
}

renderdoc\driver\ 文件夹下有

  • gl_counters.cpp
  • d3d11_counters.cpp
  • d3d12_counters.cpp
  • vk_counters.cpp

手游用到的API主要是OpenGL ES,因此跳转到gl_counters.cpp,重点查看其中的逻辑。

3、查看函数rdcarray<CounterResult> GLReplay::FetchCounters(const rdcarray<GPUCounter> &allCounters)

​ 重点关注以下几个地方:

  • 使用ctx存储结果。其类型为GLCounterContext

    struct GPUQueries
    {
      GLuint obj[arraydim<GPUCounter>()];
      uint32_t eventId;
    };
    
    struct GLCounterContext
    {
      uint32_t eventStart;
      rdcarray<GPUQueries> queries;
    };
    
  • 函数FillTimers(ctx, m_pDriver->GetRootDraw(), counters);

    • 作用

      • 填充并返回ctx。

      • 筛选出绘制类的api,以及其对应的eventId。

      • 标记两个时间戳,并在这两个时间戳之间进行绘制重播。

      // Reverse order so that Timer counter is queried the last.
      for(int32_t q = uint32_t(GPUCounter::Count) - 1; q >= 0; q--){
          if(queries->obj[q])
              // 标记开始
              m_pDriver->glBeginQuery(glCounters[q], queries->obj[q]);
      }
      
      // 根据eventId进行绘制,重播
      m_pDriver->ReplayLog(ctx.eventStart, d.eventId, eReplay_OnlyDraw);
      
      for(auto q : indices<GPUCounter>())
          if(queries->obj[q])
              // 标记结束
              m_pDriver->glEndQuery(glCounters[q]);
      
  • 函数m_pDriver->glGetQueryObjectui64v(ctx.queries[i].obj[(uint32_t)counters[c]], eGL_QUERY_RESULT, &data);

    • 作用:计算data,data中存放的值就是从标记开始到结束的时间差。也就是duration的值。
  • 将所需要的值存放进变量rdcarray<CounterResult> ret中,并返回该值。

    • 主要存放返回的值就是:eventId 和 duration

Timer Queries 小结

1、要了解的是什么?

渲染时间,或者说是调用API绘制所需的时间。

2、为什么需要了解?

可以通过这些信息来确定渲染的元素的开销占比。对于开发时调试代码的性能很有帮助。通过这种方式找到代码中最耗时的部分,以便于能够知道在哪些地方需要进行优化。

举个例子,可以通过object_time来决定是否需要在你的场景中增加或者减少物体。可以基于应用程序运行时所使用的图形硬件来决定使用更复杂的shader还是更简单的shader来渲染物体。

3、了解的方式是?

大体分为两种方法:

1)glGetQueryObjectuiv (renderdoc使用的是这种方式)

调用glGetQueryObjectuiv获取 query object的结果的时候,返回值是从调用glBeginQuery开始到调用glEndQuery之间所有的OpenGL指令耗费的时间,单位是纳秒。

// Declare our variables
GLuint queries[3]; // Three query objects that we'll use
GLuint world_time; // Time taken to draw the world
GLuint objects_time; // Time taken to draw objects in the world
GLuint HUD_time; // Time to draw the HUD and other UI elements

// Create three query objects
glGenQueries(3, queries);

glBeginQuery(GL_TIME_ELAPSED, queries[0]);
RenderWorld();
glEndQuery(GL_TIME_ELAPSED);

glBeginQuery(GL_TIME_ELAPSED, queries[1]);
RenderObjects();
glEndQuery(GL_TIME_ELAPSED);

glBeginQuery(GL_TIME_ELAPSED, queries[2]);
RenderHUD();
glEndQuery(GL_TIME_ELAPSED);

// world_time,objects_time和HUD_time这三个变量就分别存储着渲染world,所有的objects和HUD所耗费的时间。
glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &world_time);
glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &objects_time);
glGetQueryObjectuiv(queries[2], GL_QUERY_RESULT, &HUD_time);

// Clean up after ourselves.
glDeleteQueries(3, queries);

2)void glQueryCounter(GLuint id, GLenum target);

需要设置id参数为GL_TIMESTAMP,然后把target参数设置成为你之前创建的query object。这个函数会直接把查询操作推送进OpenGL的流水线中去,当OpenGL执行到那个query object的结束位置的时候,OpenGL会把当前的 时间写到query object里面去。

// Declare our variables
GLuint queries[4]; // Now we need four query objects
GLuint start_time; // The start time of the application
GLuint world_time; // Time taken to draw the world
GLuint objects_time; // Time taken to draw objects in the world
GLuint HUD_time; // Time to draw the HUD and other UI elements

// Create four query objects
glGenQueries(4, queries);// Get the start time
glQueryCounter(GL_TIMESTAMP, queries[0]);

// Render the world
RenderWorld();
// Get the time after RenderWorld is done
glQueryCounter(GL_TIMESTAMP, queries[1]);

// Render the objects in the world
RenderObjects();
// Get the time after RenderObjects is done
glQueryCounter(GL_TIMESTAMP, queries[2]);

// Render the HUD
RenderHUD();
// Get the time after everything is done
glQueryCounter(GL_TIMESTAMP, queries[3]);

// Get the result from the three queries, and subtract them to find deltas
glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &start_time);
glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &world_time);
glGetQueryObjectuiv(queries[2], GL_QUERY_RESULT, &objects_time);
glGetQueryObjectuiv(queries[3], GL_QUERY_RESULT, &HUD_time);
// 区别:需要计算出来时间差
HUD_time -= objects_time;
objects_time -= world_time;
world_time -= start_time;
// Done. world_time, objects_time, and hud_time contain the values we want.

// Clean up after ourselves.
glDeleteQueries(4, queries);

不过这两个例子的结果并不完全等同。

  • 使用GL_TIMESTAMP的方式进行查询:当OpenGL流水线执行到了query object结束位置的时候,time变量才会被写入数值。

  • 使用GL_TIME_ELAPSED的方式进行查询:OpenGL会计算出来glEndQuery指令执行结束时候的时间戳与glBeginQuery执行指令开始时的时间戳的时间差。

    很明显,结果不会是一样的。但无论采用哪种方式,拿到的时间数据都有意义。

3)优化API: void glGetQueryObjectui64v(GLuint id,GLenum pname,GLuint64 * params);

关于timer queries的一个值得注意的事就是,由于它们的计量单位是纳秒,所以它们的数值可能会非常大。如果用一个无符号的32位整数来表示纳秒数据的话,那么它顶多能玩转4秒。所以如果你的某个操作出乎预料的执行时间 超过了4秒,那么就完了。所以应该使用64位的整数来获取和存储query object的数据。

4)也可从OpenGL通过下述API来立即采用同步的方式获取一个时间戳。

GLint64 t;
// 当这代码执行完毕后,t将会保存OpenGL返回的一个当前的时间。
void glGetInteger64v(GL_TIMESTAMP, &t);

如果你先获取这个时间戳,然后立刻直接执行一个时间戳的查询,获得时间戳查询的结果后用它减掉t;这个结果将是OpenGL执行到查询操作结束位置时所花费的 时间。这也就是我们所知道的OpenGL流水线的延迟,这个延迟基本上等同于从你的应用程序调用某个指令开始,到OpenGL完全执行完毕这个指令的时间。

参考资料:https://zhuanlan.zhihu.com/p/165497334 (OpenGL)

https://docs.microsoft.com/en-us/windows/win32/api/d3d11/nn-d3d11-id3d11query (D3D11)

踩的坑

  • 如果我打断点:

    • 连接手机调试,会报错,看不到duration的最终计算数值。调用的函数是repaly_proxy.cpp 中的FetchCounters,不是我想的gl_counters.cpp

      // ReplayProxy 这个类和远程连接有关系
      rdcarray<CounterResult> ReplayProxy::FetchCounters(const rdcarray<GPUCounter> &counters)
      {
        PROXY_FUNCTION(FetchCounters, counters);
      }
      
    • 连接手机截帧完后,将截帧的文件进行保存。再用电脑打开保存到本地的.rdc文件进行调试,调用的函数是gl_counters.cpp 中的FetchCounters。但调试结束后,所有的duration值归0。(明明调试之前是有值的)

      rdcarray<CounterResult> GLReplay::FetchCounters(const rdcarray<GPUCounter> &allCounters)
      {
          // 函数逻辑比较复杂一点,可重点关注以下三个地方:
          GLCounterContext ctx;
          FillTimers(ctx, m_pDriver->GetRootDraw(), counters);
          m_pDriver->glGetQueryObjectui64v(ctx.queries[i].obj[(uint32_t)counters[c]], eGL_QUERY_RESULT, &data);
      }
      

    解决方法:

    ​ 直接用电脑端的OpenGL进行绘制、截帧、保存截帧文件、打开文件进行调试。不连接手机了,可以正常查看变量值,没有报错~

 类似资料: