草庐IT

DirectX11--CPU与GPU计时器

X_Jun 2023-03-28 原文

前言

GAMES104的王希说过:

游戏引擎的世界里,它的核心是靠Tick()函数把这个世界驱动起来。

本来单是一个CPU的计时器是不至于为其写一篇博客的,但把GPU计时器功能加上后就不一样了。在这一篇中,我们将讲述如何使用CPU计时器获取帧间隔,以及使用GPU计时器获取GPU中执行一系列指令的间隔。

DirectX11 With Windows SDK完整目录

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

CPU计时器

在游戏中,我们需要用到高精度的计时器。在这里我们直接使用龙书的GameTimer,但为了区分后续的GPU计时器,现在将其改名为CpuTimer

class CpuTimer
{
public:
    CpuTimer();
 
    float TotalTime()const;     // 返回从Reset()调用之后经过的时间,但不包括暂停期间的
    float DeltaTime()const;	    // 返回帧间隔时间

    void Reset();               // 计时开始前或者需要重置时调用
    void Start();               // 在开始计时或取消暂停的时候调用
    void Stop();                // 在需要暂停的时候调用
    void Tick();                // 在每一帧开始的时候调用
    bool IsStopped() const;     // 计时器是否暂停/结束

private:
    double m_SecondsPerCount = 0.0;
    double m_DeltaTime = -1.0;

    __int64 m_BaseTime = 0;
    __int64 m_PausedTime = 0;
    __int64 m_StopTime = 0;
    __int64 m_PrevTime = 0;
    __int64 m_CurrTime = 0;

    bool m_Stopped = false;
};

在构造函数中,我们将查询计算机performance counter的频率,因为该频率对于当前CPU是固定的,我们只需要在初始化阶段获取即可。然后我们可以求出单个count经过的时间:

CpuTimer::CpuTimer()
{
    __int64 countsPerSec{};
    QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
    m_SecondsPerCount = 1.0 / (double)countsPerSec;
}

在开始使用计数器之前,或者想要重置计时器时,我们需要调用一次Reset(),以当前时间作为基准时间。这些__int64的类型存储的单位为count:

void CpuTimer::Reset()
{
    __int64 currTime{};
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

    m_BaseTime = currTime;
    m_PrevTime = currTime;
    m_StopTime = 0;
    m_PausedTime = 0;	// 涉及到多次Reset的话需要将其归0
    m_Stopped  = false;
}

然后这里我们先看Stop()的实现,就是记录当前Stop的时间和标记为暂停中:

void CpuTimer::Stop()
{
    if( !m_Stopped )
    {
        __int64 currTime{};
        QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

        m_StopTime = currTime;
        m_Stopped  = true;
    }
}

在调用Reset()完成初始化后,我们就可以调用Start()启动计时了。当然如果之前调用过Stop()的话,将当前Stop()Start()经过的暂停时间累加到总的暂停时间:

void CpuTimer::Start()
{
    __int64 startTime{};
    QueryPerformanceCounter((LARGE_INTEGER*)&startTime);


    // 累积暂停开始到暂停结束的这段时间
    //
    //                     |<-------d------->|
    // ----*---------------*-----------------*------------> time
    //  m_BaseTime       m_StopTime        startTime     

    if( m_Stopped )
    {
        m_PausedTime += (startTime - m_StopTime);

        m_PrevTime = startTime;
        m_StopTime = 0;
        m_Stopped  = false;
    }
}

然后在每一帧开始之前调用Tick()函数,更新当前帧与上一帧之间的间隔时间,该用时通过DeltaTime()获取,可以用于物理世界的更新:

void CpuTimer::Tick()
{
    if( m_Stopped )
    {
        m_DeltaTime = 0.0;
        return;
    }

    __int64 currTime{};
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
    m_CurrTime = currTime;

    // 当前Tick与上一Tick的帧间隔
    m_DeltaTime = (m_CurrTime - m_PrevTime)*m_SecondsPerCount;

    m_PrevTime = m_CurrTime;

    if(m_DeltaTime < 0.0)
    {
        m_DeltaTime = 0.0;
    }
}

float CpuTimer::DeltaTime() const
{
    return (float)m_DeltaTime;
}

如果要获取游戏开始到现在经过的时间(不包括暂停期间),可以使用TotalTime()

float CpuTimer::TotalTime()const
{
    // 如果调用了Stop(),暂停中的这段时间我们不需要计入。此外
    // m_StopTime - m_BaseTime可能会包含之前的暂停时间,为
    // 此我们可以从m_StopTime减去之前累积的暂停的时间
    //
    //                     |<-- 暂停的时间 -->|
    // ----*---------------*-----------------*------------*------------*------> time
    //  m_BaseTime       m_StopTime        startTime     m_StopTime    m_CurrTime

    if( m_Stopped )
    {
        return (float)(((m_StopTime - m_PausedTime)-m_BaseTime)*m_SecondsPerCount);
    }

    // m_CurrTime - m_BaseTime包含暂停时间,但我们不想将它计入。
    // 为此我们可以从m_CurrTime减去之前累积的暂停的时间
    //
    //  (m_CurrTime - m_PausedTime) - m_BaseTime 
    //
    //                     |<-- 暂停的时间 -->|
    // ----*---------------*-----------------*------------*------> time
    //  m_BaseTime       m_StopTime        startTime     m_CurrTime
    
    else
    {
        return (float)(((m_CurrTime-m_PausedTime)-m_BaseTime)*m_SecondsPerCount);
    }
}

总的来说,正常的调用顺序是Reset()Start(),然后每一帧调用Tick(),并获取DeltaTime()。在需要暂停的时候就Stop(),恢复用Start()

GPU计时器

假如我们需要统计某一个渲染过程的用时,如后处理、场景渲染、阴影绘制等,可能有人的想法是这样的:

timer.Start();
DrawSomething();
timer.Tick();
float deltaTime = timer.DeltaTime();

实际上这样并不能测量,因为CPU跟GPU是异步执行的。设备上下文所调用的大部分方法实际上是向显卡塞入命令然后立刻返回,这些命令被缓存到一个命令队列中等待被消化。

因此,如果要测量GPU中一段执行过程的用时,我们需要向GPU插入两个时间戳,然后将这两个时间戳的Tick Count回读到CPU,最后通过GPU获取这期间的频率来求出间隔。

目前GpuTimer放在Common文件夹中,供36章以后的项目使用,后续会考虑放到之前的项目中。

GpuTimer类的声明如下:

class GpuTimer
{
public:
    GpuTimer() = default;
    
    // recentCount为0时统计所有间隔的平均值
    // 否则统计最近N帧间隔的平均值
    void Init(ID3D11Device* device, ID3D11DeviceContext* deviceContext, size_t recentCount = 0);
    
    // 重置平均用时
    // recentCount为0时统计所有间隔的平均值
    // 否则统计最近N帧间隔的平均值
    void Reset(ID3D11DeviceContext* deviceContext, size_t recentCount = 0);
    // 给命令队列插入起始时间戳
    HRESULT Start();
    // 给命令队列插入结束时间戳
    void Stop();
    // 尝试获取间隔
    bool TryGetTime(double* pOut);
    // 强制获取间隔(可能会造成阻塞)
    double GetTime();
    // 计算平均用时
    double AverageTime()
    {
        if (m_RecentCount)
            return m_AccumTime / m_DeltaTimes.size();
        else
            return m_AccumTime / m_AccumCount;
    }

private:
    
    static bool GetQueryDataHelper(ID3D11DeviceContext* pContext, bool loopUntilDone, ID3D11Query* query, void* data, uint32_t dataSize);
    

    std::deque<double> m_DeltaTimes;    // 最近N帧的查询间隔
    double m_AccumTime = 0.0;           // 查询间隔的累计总和
    size_t m_AccumCount = 0;            // 完成回读的查询次数
    size_t m_RecentCount = 0;           // 保留最近N帧,0则包含所有

    std::deque<GpuTimerInfo> m_Queries; // 缓存未完成的查询
    Microsoft::WRL::ComPtr<ID3D11Device> m_pDevice;
    Microsoft::WRL::ComPtr<ID3D11DeviceContext> m_pImmediateContext;
};

其中,Init()用于获取D3D设备和设备上下文,并根据recentCount确定要统计最近N帧间隔的平均值,还是所有间隔的平均值:

void GpuTimer::Init(ID3D11Device* device, ID3D11DeviceContext* deviceContext, size_t recentCount)
{
    m_pDevice = device;
    m_pImmediateContext = deviceContext;
    m_RecentCount = recentCount;
    m_AccumTime = 0.0;
    m_AccumCount = 0;
}

在调用Init()后,我们就可以开始调用Start()来给命令队列插入起始时间戳了。但在此之前,我们需要先介绍我们需要给命令队列插入的具体是什么。

ID3D11Device::CreateQuery--创建GPU查询

为了创建GPU查询,我们需要先填充D3D11_QUERY_DESC结构体:

typedef struct D3D11_QUERY_DESC {
  D3D11_QUERY Query;
  UINT        MiscFlags;  // 目前填0
} D3D11_QUERY_DESC;

关于枚举类型D3D11_QUERY,我们现在只关注其中两个枚举值:

  • D3D11_QUERY_TIMESTAMP:通过ID3D11DeviceContext::GetData返回的UINT64表示的是一个时间戳的值。该查询还需要D3D11_QUERY_TIMESTAMP_DISJOINT的配合来判断当前查询是否有效。
  • D3D11_QUERY_TIMESTAMP_DISJOINT:用来确定当前的D3D11_QUERY_TIMESTAMP是否返回可信的结果,并可以获取当前流处理器的频率,来允许你将这两个tick变换成经过的时间来求出间隔。该查询只应该在每帧或多帧中执行一次,然后通过ID3D11DeviceContext::GetData返回D3D11_QUERY_DATA_TIMESTAMP_DISJOINT

D3D11_QUERY_DATA_TIMESTAMP_DISJOINT的结构体如下:

typedef struct D3D11_QUERY_DATA_TIMESTAMP_DISJOINT {
  UINT64 Frequency;   // 当前GPU每秒增加的counter数目
  BOOL   Disjoint;    // 仅当其为false时,两个时间戳的询问才是有效的,表明这期间的频率是固定的
                      // 若为true,说明可能出现了拔开笔记本电源、过热、由于节点模式导致的功耗降低等
} D3D11_QUERY_DATA_TIMESTAMP_DISJOINT;

由于从GPU回读数据是一件很慢的事情,可能会拖慢1帧到几帧,为此我们需要把创建好的时间戳和频率/连续性查询先缓存起来。这里使用的是GpuTimerInfo

struct GpuTimerInfo
{
    D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjointData {};  // 频率/连续性信息
    uint64_t startData = 0;  // 起始时间戳
    uint64_t stopData = 0;   // 结束时间戳
    Microsoft::WRL::ComPtr<ID3D11Query> disjointQuery;    // 连续性查询
    Microsoft::WRL::ComPtr<ID3D11Query> startQuery;       // 起始时间戳查询
    Microsoft::WRL::ComPtr<ID3D11Query> stopQuery;        // 结束时间戳查询
    bool isStopped = false;                               // 是否插入了结束时间戳
};

Start()中我们需要同时创建查询、插入时间戳、开始连续性/频率查询。

HRESULT GpuTimer::Start()
{
    if (!m_Queries.empty() && !m_Queries.back().isStopped)
        return E_FAIL;

    GpuTimerInfo& info = m_Queries.emplace_back();
    CD3D11_QUERY_DESC queryDesc(D3D11_QUERY_TIMESTAMP);
    m_pDevice->CreateQuery(&queryDesc, info.startQuery.GetAddressOf());
    m_pDevice->CreateQuery(&queryDesc, info.stopQuery.GetAddressOf());
    queryDesc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT;
    m_pDevice->CreateQuery(&queryDesc, info.disjointQuery.GetAddressOf());

    m_pImmediateContext->Begin(info.disjointQuery.Get());
    m_pImmediateContext->End(info.startQuery.Get());
    return S_OK;
}

需要注意的是,D3D11_QUERY_TIMESTAMP只通过ID3D11DeviceContext::End来插入起始时间戳;D3D11_QUERY_TIMESTAMP_DISJOINT则需要区分``ID3D11DeviceContext::BeginID3D11DeviceContext::End`。

在完成某个特效渲染后,我们可以调用Stop()来插入结束时间戳,并完成连续性/频率的查询:

void GpuTimer::Stop()
{
    GpuTimerInfo& info = m_Queries.back();
    m_pImmediateContext->End(info.disjointQuery.Get());
    m_pImmediateContext->End(info.stopQuery.Get());
    info.isStopped = true;
}

调用Stop()后,这时我们还不一定能够拿到间隔。考虑到运行时的性能分析考虑的是多间隔求平均,我们可以接受延迟几帧的回读。为此,我们可以使用TryGetTime(),尝试对时间最久远、仍未完成的查询尝试GPU回读:

bool GpuTimer::GetQueryDataHelper(ID3D11DeviceContext* pContext, bool loopUntilDone, ID3D11Query* query, void* data, uint32_t dataSize)
{
    if (query == nullptr)
        return false;

    HRESULT hr = S_OK;
    int attempts = 0;
    do
    {
        // 尝试GPU回读
        hr = pContext->GetData(query, data, dataSize, 0);
        if (hr == S_OK)
            return true;
        attempts++;
        if (attempts > 100)
            Sleep(1);
        if (attempts > 1000)
        {
            assert(false);
            return false;
        }
    } while (loopUntilDone && (hr == S_FALSE));
    return false;

bool GpuTimer::TryGetTime(double* pOut)
{
    if (m_Queries.empty())
        return false;

    GpuTimerInfo& info = m_Queries.front();
    if (!info.isStopped) return false;
    if (info.disjointQuery && !GetQueryDataHelper(m_pImmediateContext.Get(), false, info.disjointQuery.Get(), &info.disjointData, sizeof(info.disjointData)))
        return false;
    info.disjointQuery.Reset();

    if (info.startQuery && !GetQueryDataHelper(m_pImmediateContext.Get(), false, info.startQuery.Get(), &info.startData, sizeof(info.startData)))
        return false;
    info.startQuery.Reset();

    if (info.stopQuery && !GetQueryDataHelper(m_pImmediateContext.Get(), false, info.stopQuery.Get(), &info.stopData, sizeof(info.stopData)))
        return false;
    info.stopQuery.Reset();

    if (!info.disjointData.Disjoint)
    {
        double deltaTime = static_cast<double>(info.stopData - info.startData) / info.disjointData.Frequency;
        if (m_RecentCount > 0)
            m_DeltaTimes.push_back(deltaTime);
        m_AccumTime += deltaTime;
        m_AccumCount++;
        if (m_DeltaTimes.size() > m_RecentCount)
        {
            m_AccumTime -= m_DeltaTimes.front();
            m_DeltaTimes.pop_front();
        }
        if (pOut) *pOut = deltaTime;
    }
    else
    {
        double deltaTime = -1.0;
    }

    m_Queries.pop_front();
    return true;
}

如果你就是在当前帧获取间隔,可以使用GetTime()

double GpuTimer::GetTime()
{
    if (m_Queries.empty())
        return -1.0;

    GpuTimerInfo& info = m_Queries.front();
    if (!info.isStopped) return -1.0;

    if (info.disjointQuery)
    {
        GetQueryDataHelper(m_pImmediateContext.Get(), true, info.disjointQuery.Get(), &info.disjointData, sizeof(info.disjointData));
        info.disjointQuery.Reset();
    }
    if (info.startQuery)
    {
        GetQueryDataHelper(m_pImmediateContext.Get(), true, info.startQuery.Get(), &info.startData, sizeof(info.startData));
        info.startQuery.Reset();
    }
    if (info.stopQuery)
    {
        GetQueryDataHelper(m_pImmediateContext.Get(), true, info.stopQuery.Get(), &info.stopData, sizeof(info.stopData));
        info.stopQuery.Reset();
    }

    double deltaTime = -1.0;
    if (!info.disjointData.Disjoint)
    {
        deltaTime = static_cast<double>(info.stopData - info.startData) / info.disjointData.Frequency;
        if (m_RecentCount > 0)
            m_DeltaTimes.push_back(deltaTime);
        m_AccumTime += deltaTime;
        m_AccumCount++;
        if (m_DeltaTimes.size() > m_RecentCount)
        {
            m_AccumTime -= m_DeltaTimes.front();
            m_DeltaTimes.pop_front();
        }
    }

    m_Queries.pop_front();
    return deltaTime;
}

重置GPU计时器的话使用Reset()方法:

void GpuTimer::Reset(ID3D11DeviceContext* deviceContext, size_t recentCount)
{
    m_Queries.clear();
    m_DeltaTimes.clear();
    m_pImmediateContext = deviceContext;
    m_AccumTime = 0.0;
    m_AccumCount = 0;
    if (recentCount)
        m_RecentCount = recentCount;
}

下面的代码展示如何使用GPU计时器:

m_GpuTimer.Init(m_pd3dDevice.Get(), m_pd3dImmediateContext.Get());



// ...
m_GpuTimer.Start();
{
    // 一些绘制过程...
}
m_GpuTimer.Stop();

// ...
m_GpuTimer.TryGetTime(nullptr); // 只是为了更新下面的平均值
float avgTime = m_GpuTimer.AverageTime();

下面是分块延迟渲染统计各个pass用时的例子:

注意:如果游戏开启了垂直同步,那么当前帧中的某一个查询很可能会受到垂直同步的影响被拖长,从而导致原本当前帧GPU计时器的平均用时总和会接近两个垂直同步信号的间隔。以下图为例

DirectX11 With Windows SDK完整目录

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

有关DirectX11--CPU与GPU计时器的更多相关文章

  1. ruby - 安装libv8(3.11.8.13)出错,Bundler无法继续 - 2

    运行bundleinstall后出现此错误:Gem::Package::FormatError:nometadatafoundin/Users/jeanosorio/.rvm/gems/ruby-1.9.3-p286/cache/libv8-3.11.8.13-x86_64-darwin-12.gemAnerroroccurredwhileinstallinglibv8(3.11.8.13),andBundlercannotcontinue.Makesurethat`geminstalllibv8-v'3.11.8.13'`succeedsbeforebundling.我试试gemin

  2. ruby - Ruby 性能中的计时器 - 2

    我正在寻找一个用ruby​​演示计时器的在线示例,并发现了下面的代码。它按预期工作,但这个简单的程序使用30Mo内存(如Windows任务管理器中所示)和太多CPU有意义吗?非常感谢deftime_blockstart_time=Time.nowThread.new{yield}Time.now-start_timeenddefrepeat_every(seconds)whiletruedotime_spent=time_block{yield}#Tohandle-vesleepinteravalsleep(seconds-time_spent)iftime_spent

  3. ruby - ri 有空文件 – Ubuntu 11.10, Ruby 1.9 - 2

    我正在运行Ubuntu11.10并像这样安装Ruby1.9:$sudoapt-getinstallruby1.9rubygems一切都运行良好,但ri似乎有空文档。ri告诉我文档是空的,我必须安装它们。我执行此操作是因为我读到它会有所帮助:$rdoc--all--ri现在,当我尝试打开任何文档时:$riArrayNothingknownaboutArray我搜索的其他所有内容都是一样的。 最佳答案 这个呢?apt-getinstallri1.8编辑或者试试这个:(非rvm)geminstallrdocrdoc-datardoc-da

  4. ruby - rails 3.2.2(或 3.2.1)+ Postgresql 9.1.3 + Ubuntu 11.10 连接错误 - 2

    我正在使用PostgreSQL9.1.3(x86_64-pc-linux-gnu上的PostgreSQL9.1.3,由gcc-4.6.real(Ubuntu/Linaro4.6.1-9ubuntu3)4.6.1,64位编译)和在ubuntu11.10上运行3.2.2或3.2.1。现在,我可以使用以下命令连接PostgreSQLsupostgres输入密码我可以看到postgres=#我将以下详细信息放在我的config/database.yml中并执行“railsdb”,它工作正常。开发:adapter:postgresqlencoding:utf8reconnect:falsedat

  5. ruby-on-rails - Rails 2.3.11 DateTime BigDecimal 精度 - 2

    我目前有一个运行Ruby1.8.7和Rails2.3.2的RubyonRails项目我有一些从数据库中读取数据的单元测试,特别是两个连续项目的日期时间列,这两个项目应该相隔24小时。在一项测试中,我将项目2的日期时间设置为与项目1的日期时间相同。当我执行断言以确保两个值相等时,测试在rails2.3.2下工作正常。当我升级到rails2.3.11时,测试失败显示两次之间的差异将关闭并出现以下错误:expectedbutwas.这两个版本的rails中似乎存在浮点转换问题。如何解决float问题? 最佳答案 这也发生在我身上,我最终这

  6. Win10 / 11新电脑最简单跳过联网激活和使用本地账户登录方法 - 2

    跳过联网激活:OOBE界面直接按Ctrl+Shift+F3进入审核模式。这样就可以直接进入系统进行一些硬件测试等,而不用联网激活导致新机无法退货。需要注意的是,在审核模式下进行的一些操作都会保留,并不会在退出后自动还原!安装的软件在正常开机进系统后还会看见!如果电脑确实没连互联网又不想强行跳过OOBE(网上很多教程会叫你直接结束OOBE进程,但这是不推荐的,因为一些厂商自带优化程序和系统初始化设置在后面都会应用,对于笔记本跳过的话你会发现驱动和内置应用都没有装上。其实这部分脚本就在系统盘的Recovery隐藏文件夹下),可以参考以下方式:https://www.landiannews.com/

  7. ruby-on-rails - 负载测试期间 Unicorn CPU 使用率激增,优化方法 - 2

    我对为我的RubyonRails3.1.3应用优化我的Unicorn设置的方法很感兴趣。我目前正在高CPU超大实例上生成14个工作进程,因为我的应用程序在负载测试期间似乎受CPU限制。在模拟负载测试中,每秒大约20个请求重放请求,我的实例上的所有8个内核都达到峰值,盒子负载飙升至7-8个。每个unicorn实例使用大约56-60%的CPU。我很好奇可以通过哪些方式对其进行优化?我希望能够每秒将更多请求汇集到这种大小的实例上。内存和所有其他I/O一样完全正常。在我的测试过程中,CPU越来越低。 最佳答案 如果您受CPU限制,您希望使用

  8. ruby - Ruby 进程如何限制其 CPU 使用率? - 2

    假设我希望Ruby进程使用的CPU不超过15%。是否可以?怎么办? 最佳答案 您可以尝试使用Process.setrlimit来自标准核心:Setstheresourcelimitoftheprocess.这看起来只是setrlimit的包装器来自C库,因此它可能仅在Unix-ish平台上可用。setrlimit不支持CPU百分比限制,但它支持以秒为单位限制CPU时间。如果您只是想让您的Ruby进程不占用整个CPU,那么您可以尝试使用Process.setpriority来调整它的优先级。这只是libc的setpriority的包装

  9. ruby-on-rails - 在 El Capitan 上安装 Rails 时出现 -lgmp 错误的库未找到(Mac OS 10.11.1 (15B42)) - 2

    在使用Rubyv2.2.2的ElCapitan(MacOSX10.11.1)上安装Rails时,出现以下错误:ERROR:Errorinstallingnokogiri:ERROR:Failedtobuildgemnativeextension./Users/jon/.rvm/rubies/ruby-2.2.2/bin/ruby-r./siteconf20151117-26799-ux15fd.rbextconf.rb--use-system-librariescheckingiftheCcompileraccepts...***extconf.rbfailed***Couldnotc

  10. ruby-on-rails - Rails 3.2.11 突然需要重启到 'acknowledge' Controller 有什么变化吗? - 2

    标题说明了一切。请注意,这不是模型或初始值设定项的更改。我可以删除Controller中的一个实例变量(例如,@user),然后重新加载一个View,它会工作-直到我重新启动服务器,在这种情况下它会提示变量为nil。我正常工作,然后切换到一组完全不同的Controller和View上工作,现在它无缘无故地发生了。应用处于开发环境中。development.rb内容:Dashboard::Application.configuredoconfig.cache_classes=falseconfig.whiny_nils=trueconfig.consider_all_requests_l

随机推荐