This commit is contained in:
xsl
2025-09-04 10:54:47 +08:00
commit 6bc8f61b18
1808 changed files with 208268 additions and 0 deletions
@@ -0,0 +1,43 @@
/* Copyright (c) 2020, Broadcom Inc. and Contributors
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "frame_time_stats_provider.h"
namespace vkb
{
FrameTimeStatsProvider::FrameTimeStatsProvider(std::set<StatIndex> &requested_stats)
{
// We always, and only, support StatIndex::frame_times since it's handled directly by us.
// Remove from requested set to stop other providers looking for it.
requested_stats.erase(StatIndex::frame_times);
}
bool FrameTimeStatsProvider::is_available(StatIndex index) const
{
// We only support StatIndex::frame_times
return index == StatIndex::frame_times;
}
StatsProvider::Counters FrameTimeStatsProvider::sample(float delta_time)
{
Counters res;
// frame_times comes directly from delta_time
res[StatIndex::frame_times].result = delta_time;
return res;
}
} // namespace vkb
@@ -0,0 +1,46 @@
/* Copyright (c) 2020-2025, Broadcom Inc. and Contributors
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "stats_provider.h"
#include <set>
namespace vkb
{
class FrameTimeStatsProvider : public StatsProvider
{
public:
/**
* @brief Constructs a FrameTimeStatsProvider
* @param requested_stats Set of stats to be collected. Supported stats will be removed from the set.
*/
FrameTimeStatsProvider(std::set<StatIndex> &requested_stats);
/**
* @brief Checks if this provider can supply the given enabled stat
* @param index The stat index
* @return True if the stat is available, false otherwise
*/
bool is_available(StatIndex index) const override;
/**
* @brief Retrieve a new sample set
* @param delta_time Time since last sample
*/
Counters sample(float delta_time) override;
};
} // namespace vkb
+60
View File
@@ -0,0 +1,60 @@
/* Copyright (c) 2021-2025, NVIDIA CORPORATION. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <stats/stats.h>
#include <rendering/hpp_render_context.h>
namespace vkb
{
namespace stats
{
/**
* @brief facade class around vkb::Stats, providing a vulkan.hpp-based interface
*
* See vkb::Stats for documentation
*/
class HPPStats : private vkb::Stats
{
public:
using vkb::Stats::get_data;
using vkb::Stats::get_graph_data;
using vkb::Stats::get_requested_stats;
using vkb::Stats::is_available;
using vkb::Stats::request_stats;
using vkb::Stats::resize;
using vkb::Stats::update;
explicit HPPStats(vkb::rendering::HPPRenderContext &render_context, size_t buffer_size = 16) :
vkb::Stats(reinterpret_cast<vkb::RenderContext &>(render_context), buffer_size)
{}
void begin_sampling(vkb::core::CommandBufferCpp &cb)
{
vkb::Stats::begin_sampling(reinterpret_cast<vkb::core::CommandBufferC &>(cb));
}
void end_sampling(vkb::core::CommandBufferCpp &cb)
{
vkb::Stats::end_sampling(reinterpret_cast<vkb::core::CommandBufferC &>(cb));
}
};
} // namespace stats
} // namespace vkb
+274
View File
@@ -0,0 +1,274 @@
/* Copyright (c) 2018-2025, Arm Limited and Contributors
* Copyright (c) 2020-2025, Broadcom Inc.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "hwcpipe_stats_provider.h"
namespace
{
const char *get_product_family_name(hwcpipe::device::gpu_family f)
{
using gpu_family = hwcpipe::device::gpu_family;
switch (f)
{
case gpu_family::bifrost:
return "Bifrost";
case gpu_family::midgard:
return "Midgard";
case gpu_family::valhall:
return "Valhall";
default:
return "Unknown";
}
}
} // namespace
namespace vkb
{
HWCPipeStatsProvider::HWCPipeStatsProvider(std::set<StatIndex> &requested_stats)
{
// Mapping of stats to their hwcpipe availability
// clang-format off
StatDataMap hwcpipe_stats = {
{StatIndex::gpu_cycles, {hwcpipe_counter::MaliGPUActiveCy}},
{StatIndex::gpu_vertex_cycles, {hwcpipe_counter::MaliNonFragQueueActiveCy, {MaliNonFragActiveCy, MaliBinningQueueActiveCy} }},
{StatIndex::gpu_load_store_cycles, {hwcpipe_counter::MaliLSIssueCy}},
{StatIndex::gpu_tiles, {hwcpipe_counter::MaliFragTile}},
{StatIndex::gpu_killed_tiles, {hwcpipe_counter::MaliFragTileKill}},
{StatIndex::gpu_fragment_cycles, {hwcpipe_counter::MaliFragQueueActiveCy, {MaliFragActiveCy, MaliMainQueueActiveCy}}},
{StatIndex::gpu_fragment_jobs, {hwcpipe_counter::MaliFragQueueJob, {MaliMainQueueJob}}},
{StatIndex::gpu_ext_reads, {hwcpipe_counter::MaliExtBusRdBt}},
{StatIndex::gpu_ext_writes, {hwcpipe_counter::MaliExtBusWrBt}},
{StatIndex::gpu_ext_read_stalls, {hwcpipe_counter::MaliExtBusRdStallCy}},
{StatIndex::gpu_ext_write_stalls, {hwcpipe_counter::MaliExtBusWrStallCy}},
{StatIndex::gpu_ext_read_bytes, {hwcpipe_counter::MaliExtBusRdBy}},
{StatIndex::gpu_ext_write_bytes, {hwcpipe_counter::MaliExtBusWrBy}},
{StatIndex::gpu_tex_cycles, {hwcpipe_counter::MaliTexIssueCy}}};
// clang-format on
// Detect all GPUs & print some info
for (const auto &gpu : hwcpipe::find_gpus())
{
LOGI("HWCPipe: ------------------------------------------------------------");
LOGI("HWCPipe: GPU Device {}:", gpu.get_device_number());
LOGI("HWCPipe: ------------------------------------------------------------");
LOGI("HWCPipe: Product Family: {}", get_product_family_name(gpu.get_gpu_family()));
LOGI("HWCPipe: Number of Cores: {}", gpu.num_shader_cores());
LOGI("HWCPipe: Bus Width: {}", gpu.bus_width());
}
// Probe device 0 (i.e. /dev/mali0)
auto gpu = hwcpipe::gpu(0);
if (!gpu)
{
LOGE("HWCPipe: Mali GPU device 0 is missing");
}
auto config = hwcpipe::sampler_config(gpu);
auto counter_db = hwcpipe::counter_database{};
std::error_code ec;
for (const auto &stat : requested_stats)
{
auto it = hwcpipe_stats.find(stat);
if (it != hwcpipe_stats.end())
{
hwcpipe::counter_metadata meta;
auto ec = counter_db.describe_counter(it->second.counter, meta);
if (ec)
{
LOGE("HWCPipe: unknown counter");
}
ec = config.add_counter(it->second.counter);
if (ec)
{
// Some counters have changed in recent devices
auto &fallback_list = it->second.fallback_list;
if (!fallback_list.empty())
{
for (const auto &fallback : fallback_list)
{
ec = counter_db.describe_counter(fallback, meta);
ec = config.add_counter(fallback);
if (!ec)
{
// Replace counter with available alternative
it->second.counter = fallback;
break;
}
}
}
}
if (ec)
{
LOGE("HWCPipe: '{}' counter not supported by this GPU.", meta.name);
}
else
{
stat_data[stat] = hwcpipe_stats[stat];
LOGI("HWCPipe: enabled '{}' counter", meta.name);
}
}
}
// Remove any supported stats from the requested set.
// Subsequent providers will then only look for things that aren't already supported.
for (const auto &iter : stat_data)
{
requested_stats.erase(iter.first);
}
stat_data_count = stat_data.size();
sampler = std::make_unique<hwcpipe::sampler<>>(config);
if (stat_data_count > 0)
{
ec = sampler->start_sampling();
if (ec)
{
LOGE("HWCPipe: {}", ec.message());
}
}
}
HWCPipeStatsProvider::~HWCPipeStatsProvider()
{
std::error_code ec;
if (stat_data_count > 0)
{
ec = sampler->stop_sampling();
if (ec)
{
LOGE("HWCPipe: {}", ec.message());
}
}
}
bool HWCPipeStatsProvider::is_available(StatIndex index) const
{
return stat_data.find(index) != stat_data.end();
}
const StatGraphData &HWCPipeStatsProvider::get_graph_data(StatIndex index) const
{
assert(is_available(index) && "HWCPipeStatsProvider::get_graph_data() called with invalid StatIndex");
static StatGraphData vertex_compute_cycles{"Vertex Compute Cycles", "{:4.1f} M/s", static_cast<float>(1e-6)};
// HWCPipe reports combined vertex/compute cycles (which is Arm specific)
// Ensure we report graph with the correct name when asked for vertex cycles
if (index == StatIndex::gpu_vertex_cycles)
{
return vertex_compute_cycles;
}
return default_graph_map[index];
}
static double get_gpu_counter_value(const hwcpipe::counter_sample &sample)
{
switch (sample.type)
{
case hwcpipe::counter_sample::type::uint64:
return sample.value.uint64;
case hwcpipe::counter_sample::type::float64:
return sample.value.float64;
default:
return 0.0;
}
}
StatsProvider::Counters HWCPipeStatsProvider::sample(float delta_time)
{
Counters res;
if (stat_data_count < 1)
{
return res;
}
std::error_code ec;
ec = sampler->sample_now();
if (ec)
{
LOGE("HWCPipe: {}", ec.message());
}
else
{
// Map from hwcpipe measurement to our sample result for each counter
for (auto iter : stat_data)
{
StatIndex index = iter.first;
const StatData &data = iter.second;
hwcpipe::counter_sample sample;
ec = sampler->get_counter_value(data.counter, sample);
if (ec)
{
LOGE("HWCPipe: {}", ec.message());
continue;
}
auto d = get_gpu_counter_value(sample);
if (data.scaling == StatScaling::ByDeltaTime && delta_time != 0.0f)
{
d /= delta_time;
}
else if (data.scaling == StatScaling::ByCounter)
{
ec = sampler->get_counter_value(data.divisor, sample);
if (ec)
{
LOGE("HWCPipe: {}", ec.message());
continue;
}
double divisor = get_gpu_counter_value(sample);
if (divisor != 0.0)
{
d /= divisor;
}
else
{
d = 0.0;
}
}
res[index].result = d;
}
}
return res;
}
StatsProvider::Counters HWCPipeStatsProvider::continuous_sample(float delta_time)
{
return sample(delta_time);
}
} // namespace vkb
+105
View File
@@ -0,0 +1,105 @@
/* Copyright (c) 2018-2025, Arm Limited and Contributors
* Copyright (c) 2020-2025, Broadcom Inc.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "common/error.h"
#include "common/vk_common.h"
#include "stats_provider.h"
VKBP_DISABLE_WARNINGS()
#include <device/product_id.hpp>
#include <hwcpipe/counter_database.hpp>
#include <hwcpipe/gpu.hpp>
#include <hwcpipe/sampler.hpp>
#include <iomanip>
#include <unistd.h>
VKBP_ENABLE_WARNINGS()
namespace vkb
{
class HWCPipeStatsProvider : public StatsProvider
{
private:
struct StatData
{
hwcpipe_counter counter;
std::vector<hwcpipe_counter> fallback_list;
StatScaling scaling;
hwcpipe_counter divisor;
StatData(hwcpipe_counter _counter = {}, std::vector<hwcpipe_counter> _fb_list = {}, StatScaling _sc = {}, hwcpipe_counter div = {}) :
counter(_counter), fallback_list(std::move(_fb_list)), scaling(_sc), divisor(div)
{}
};
using StatDataMap = std::unordered_map<StatIndex, StatData, StatIndexHash>;
public:
/**
* @brief Constructs a HWCPipeStateProvider
* @param requested_stats Set of stats to be collected. Supported stats will be removed from the set.
*/
HWCPipeStatsProvider(std::set<StatIndex> &requested_stats);
/**
* @brief Destructor
*/
~HWCPipeStatsProvider();
/**
* @brief Checks if this provider can supply the given enabled stat
* @param index The stat index
* @return True if the stat is available, false otherwise
*/
bool is_available(StatIndex index) const override;
/**
* @brief Retrieve graphing data for the given enabled stat
* @param index The stat index
*/
const StatGraphData &get_graph_data(StatIndex index) const override;
/**
* @brief Retrieve a new sample set from polled sampling
* @param delta_time Time since last sample
*/
Counters sample(float delta_time) override;
/**
* @brief Retrieve a new sample set from continuous sampling
* @param delta_time Time since last sample
*/
Counters continuous_sample(float delta_time) override;
private:
std::unique_ptr<hwcpipe::sampler<>> sampler;
size_t stat_data_count{0};
// Only stats which are available and were requested end up in stat_data
StatDataMap stat_data;
// Counter sampling configuration
CounterSamplingConfig sampling_config;
};
} // namespace vkb
+476
View File
@@ -0,0 +1,476 @@
/* Copyright (c) 2018-2025, Arm Limited and Contributors
* Copyright (c) 2020-2025, Broadcom Inc.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "stats/stats.h"
#include <core/util/profiling.hpp>
#include <vk_mem_alloc.h>
#include <vulkan/vulkan.hpp>
#include "core/device.h"
#include "frame_time_stats_provider.h"
#ifdef VK_USE_PLATFORM_ANDROID_KHR
# include "hwcpipe_stats_provider.h"
#endif
#include "core/allocated.h"
#include "rendering/render_context.h"
#include "vulkan_stats_provider.h"
namespace vkb
{
Stats::Stats(RenderContext &render_context, size_t buffer_size) :
render_context(render_context),
buffer_size(buffer_size)
{
assert(buffer_size >= 2 && "Buffers size should be greater than 2");
}
Stats::~Stats()
{
if (stop_worker)
{
stop_worker->set_value();
}
if (worker_thread.joinable())
{
worker_thread.join();
}
}
void Stats::request_stats(const std::set<StatIndex> &wanted_stats,
CounterSamplingConfig config)
{
if (providers.size() != 0)
{
throw std::runtime_error("Stats must only be requested once");
}
requested_stats = wanted_stats;
sampling_config = config;
// Copy the requested stats, so they can be changed by the providers below
std::set<StatIndex> stats = requested_stats;
// Initialize our list of providers (in priority order)
// All supported stats will be removed from the given 'stats' set by the provider's constructor
// so subsequent providers only see requests for stats that aren't already supported.
providers.emplace_back(std::make_unique<FrameTimeStatsProvider>(stats));
#ifdef VK_USE_PLATFORM_ANDROID_KHR
providers.emplace_back(std::make_unique<HWCPipeStatsProvider>(stats));
#endif
providers.emplace_back(std::make_unique<VulkanStatsProvider>(stats, sampling_config, render_context));
// In continuous sampling mode we still need to update the frame times as if we are polling
// Store the frame time provider here so we can easily access it later.
frame_time_provider = providers[0].get();
for (const auto &stat : requested_stats)
{
counters[stat] = std::vector<float>(buffer_size, 0);
}
if (sampling_config.mode == CounterSamplingMode::Continuous)
{
// Start a thread for continuous sample capture
stop_worker = std::make_unique<std::promise<void>>();
worker_thread = std::thread([this] {
continuous_sampling_worker(stop_worker->get_future());
});
// Reduce smoothing for continuous sampling
alpha_smoothing = 0.6f;
}
for (const auto &stat_index : requested_stats)
{
if (!is_available(stat_index))
{
LOGW(vkb::StatsProvider::default_graph_data(stat_index).name + " : not available");
}
}
}
void Stats::resize(const size_t width)
{
// The circular buffer size will be 1/16th of the width of the screen
// which means every sixteen pixels represent one graph value
buffer_size = width >> 4;
for (auto &counter : counters)
{
counter.second.resize(buffer_size);
counter.second.shrink_to_fit();
}
}
bool Stats::is_available(const StatIndex index) const
{
for (const auto &p : providers)
{
if (p->is_available(index))
{
return true;
}
}
return false;
}
static void add_smoothed_value(std::vector<float> &values, float value, float alpha)
{
assert(values.size() >= 2 && "Buffers size should be greater than 2");
if (values.size() == values.capacity())
{
// Shift values to the left to make space at the end and update counters
std::rotate(values.begin(), values.begin() + 1, values.end());
}
// Use an exponential moving average to smooth values
values.back() = value * alpha + *(values.end() - 2) * (1.0f - alpha);
}
void Stats::update(float delta_time)
{
switch (sampling_config.mode)
{
case CounterSamplingMode::Polling:
{
StatsProvider::Counters sample;
for (auto &p : providers)
{
auto s = p->sample(delta_time);
sample.insert(s.begin(), s.end());
}
push_sample(sample);
break;
}
case CounterSamplingMode::Continuous:
{
// Check that we have no pending samples to be shown
if (pending_samples.size() == 0)
{
std::unique_lock<std::mutex> lock(continuous_sampling_mutex);
if (!should_add_to_continuous_samples)
{
// If we have no pending samples, we let the worker thread
// capture samples for the next frame
should_add_to_continuous_samples = true;
}
else
{
// The worker thread has captured a frame, so we stop it
// and read the samples
should_add_to_continuous_samples = false;
pending_samples.clear();
std::swap(pending_samples, continuous_samples);
}
}
if (pending_samples.size() == 0)
{
return;
}
// Ensure the number of pending samples is capped at a reasonable value
if (pending_samples.size() > 100)
{
// Prefer later samples over new samples.
std::move(pending_samples.end() - 100, pending_samples.end(), pending_samples.begin());
pending_samples.erase(pending_samples.begin() + 100, pending_samples.end());
// If we get to this point, we're not reading samples fast enough, nudge a little ahead.
fractional_pending_samples += 1.0f;
}
// Compute the number of samples to show this frame
float floating_sample_count = sampling_config.speed * delta_time * static_cast<float>(buffer_size) + fractional_pending_samples;
// Keep track of the fractional value to avoid speeding up or slowing down too much due to rounding errors.
// Generally we push very few samples per frame, so this matters.
fractional_pending_samples = floating_sample_count - std::floor(floating_sample_count);
auto sample_count = static_cast<size_t>(floating_sample_count);
// Clamp the number of samples
sample_count = std::max<size_t>(1, std::min<size_t>(sample_count, pending_samples.size()));
// Get the frame time stats (not a continuous stat)
StatsProvider::Counters frame_time_sample = frame_time_provider->sample(delta_time);
// Push the samples to circular buffers
std::for_each(pending_samples.begin(), pending_samples.begin() + sample_count, [this, frame_time_sample](auto &s) {
// Write the correct frame time into the continuous stats
s.insert(frame_time_sample.begin(), frame_time_sample.end());
// Then push the sample to the counters list
this->push_sample(s);
});
pending_samples.erase(pending_samples.begin(), pending_samples.begin() + sample_count);
break;
}
}
profile_counters();
}
void Stats::continuous_sampling_worker(std::future<void> should_terminate)
{
worker_timer.tick();
for (auto &p : providers)
{
p->continuous_sample(0.0f);
}
while (should_terminate.wait_for(std::chrono::seconds(0)) != std::future_status::ready)
{
auto delta_time = static_cast<float>(worker_timer.tick());
auto interval = std::chrono::duration_cast<std::chrono::duration<float>>(sampling_config.interval).count();
// Ensure we wait for the interval specified in config
if (delta_time < interval)
{
std::this_thread::sleep_for(std::chrono::duration<float>(interval - delta_time));
delta_time += static_cast<float>(worker_timer.tick());
}
// Sample counters
StatsProvider::Counters sample;
for (auto &p : providers)
{
StatsProvider::Counters s = p->continuous_sample(delta_time);
sample.insert(s.begin(), s.end());
}
// Add the new sample to the vector of continuous samples
{
std::unique_lock<std::mutex> lock(continuous_sampling_mutex);
if (should_add_to_continuous_samples)
{
continuous_samples.push_back(sample);
}
}
}
}
void Stats::push_sample(const StatsProvider::Counters &sample)
{
for (auto &c : counters)
{
StatIndex idx = c.first;
std::vector<float> &values = c.second;
// Find the counter matching this StatIndex in the Sample
const auto &smp = sample.find(idx);
if (smp == sample.end())
{
continue;
}
float measurement = static_cast<float>(smp->second.result);
add_smoothed_value(values, measurement, alpha_smoothing);
}
}
namespace
{
// For now names are taken from the stats_provider.cpp file
const char *to_string(StatIndex index)
{
switch (index)
{
case StatIndex::frame_times:
return "Frame Times (ms)";
case StatIndex::cpu_cycles:
return "CPU Cycles (M/s)";
case StatIndex::cpu_instructions:
return "CPU Instructions (M/s)";
case StatIndex::cpu_cache_miss_ratio:
return "Cache Miss Ratio (%)";
case StatIndex::cpu_branch_miss_ratio:
return "Branch Miss Ratio (%)";
case StatIndex::cpu_l1_accesses:
return "CPU L1 Accesses (M/s)";
case StatIndex::cpu_instr_retired:
return "CPU Instructions Retired (M/s)";
case StatIndex::cpu_l2_accesses:
return "CPU L2 Accesses (M/s)";
case StatIndex::cpu_l3_accesses:
return "CPU L3 Accesses (M/s)";
case StatIndex::cpu_bus_reads:
return "CPU Bus Read Beats (M/s)";
case StatIndex::cpu_bus_writes:
return "CPU Bus Write Beats (M/s)";
case StatIndex::cpu_mem_reads:
return "CPU Memory Read Instructions (M/s)";
case StatIndex::cpu_mem_writes:
return "CPU Memory Write Instructions (M/s)";
case StatIndex::cpu_ase_spec:
return "CPU Speculatively Exec. SIMD Instructions (M/s)";
case StatIndex::cpu_vfp_spec:
return "CPU Speculatively Exec. FP Instructions (M/s)";
case StatIndex::cpu_crypto_spec:
return "CPU Speculatively Exec. Crypto Instructions (M/s)";
case StatIndex::gpu_cycles:
return "GPU Cycles (M/s)";
case StatIndex::gpu_vertex_cycles:
return "Vertex Cycles (M/s)";
case StatIndex::gpu_load_store_cycles:
return "Load Store Cycles (k/s)";
case StatIndex::gpu_tiles:
return "Tiles (k/s)";
case StatIndex::gpu_killed_tiles:
return "Tiles killed by CRC match (k/s)";
case StatIndex::gpu_fragment_jobs:
return "Fragment Jobs (s)";
case StatIndex::gpu_fragment_cycles:
return "Fragment Cycles (M/s)";
case StatIndex::gpu_tex_cycles:
return "Shader Texture Cycles (k/s)";
case StatIndex::gpu_ext_reads:
return "External Reads (M/s)";
case StatIndex::gpu_ext_writes:
return "External Writes (M/s)";
case StatIndex::gpu_ext_read_stalls:
return "External Read Stalls (M/s)";
case StatIndex::gpu_ext_write_stalls:
return "External Write Stalls (M/s)";
case StatIndex::gpu_ext_read_bytes:
return "External Read Bytes (MiB/s)";
case StatIndex::gpu_ext_write_bytes:
return "External Write Bytes (MiB/s)";
default:
return nullptr;
}
}
} // namespace
void Stats::profile_counters() const
{
#if VKB_PROFILING
static std::chrono::high_resolution_clock::time_point last_time = std::chrono::high_resolution_clock::now();
std::chrono::high_resolution_clock::time_point now = std::chrono::high_resolution_clock::now();
if (now - last_time < std::chrono::milliseconds(100))
{
return;
}
last_time = now;
for (auto &c : counters)
{
StatIndex idx = c.first;
auto &graph_data = get_graph_data(idx);
if (c.second.empty())
{
continue;
}
float average = 0.0f;
for (auto &v : c.second)
{
average += v;
}
average /= c.second.size();
if (auto *index_name = to_string(idx))
{
Plot<float>::plot(index_name, average * graph_data.scale_factor);
}
}
static std::vector<std::string> labels;
auto &device = render_context.get_device();
VmaAllocator allocator = allocated::get_memory_allocator();
VmaBudget heap_budgets[VK_MAX_MEMORY_HEAPS];
vmaGetHeapBudgets(allocator, heap_budgets);
// We know that we will only ever have one device in the system, so we can cache the labels
if (labels.size() == 0)
{
VkPhysicalDeviceMemoryProperties memory_properties;
vkGetPhysicalDeviceMemoryProperties(device.get_gpu().get_handle(), &memory_properties);
labels.reserve(memory_properties.memoryHeapCount);
for (size_t heap = 0; heap < memory_properties.memoryHeapCount; heap++)
{
VkMemoryPropertyFlags flags = memory_properties.memoryHeaps[heap].flags;
labels.push_back("Heap " + std::to_string(heap) + " " + vk::to_string(vk::MemoryPropertyFlags{flags}));
}
}
for (size_t heap = 0; heap < labels.size(); heap++)
{
Plot<float, PlotType::Memory>::plot(labels[heap].c_str(), heap_budgets[heap].usage / (1024.0f * 1024.0f));
}
#endif
}
void Stats::begin_sampling(vkb::core::CommandBufferC &cb)
{
// Inform the providers
for (auto &p : providers)
{
p->begin_sampling(cb);
}
}
void Stats::end_sampling(vkb::core::CommandBufferC &cb)
{
// Inform the providers
for (auto &p : providers)
{
p->end_sampling(cb);
}
}
const StatGraphData &Stats::get_graph_data(StatIndex index) const
{
for (auto &p : providers)
{
if (p->is_available(index))
{
return p->get_graph_data(index);
}
}
return StatsProvider::default_graph_data(index);
}
StatGraphData::StatGraphData(const std::string &name,
const std::string &graph_label_format,
float scale_factor,
bool has_fixed_max,
float max_value) :
name(name),
format{graph_label_format},
scale_factor{scale_factor},
has_fixed_max{has_fixed_max},
max_value{max_value}
{
}
} // namespace vkb
+204
View File
@@ -0,0 +1,204 @@
/* Copyright (c) 2018-2025, Arm Limited and Contributors
* Copyright (c) 2020-2025, Broadcom Inc.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <cstdint>
#include <ctime>
#include <future>
#include <map>
#include <set>
#include <vector>
#include "stats_common.h"
#include "stats_provider.h"
#include "timer.h"
namespace vkb
{
class RenderContext;
namespace core
{
template <vkb::BindingType bindingType>
class CommandBuffer;
using CommandBufferC = CommandBuffer<vkb::BindingType::C>;
} // namespace core
/*
* @brief Helper class for querying statistics about the CPU and the GPU
*/
class Stats
{
public:
/**
* @brief Constructs a Stats object
* @param render_context The RenderContext for this sample
* @param buffer_size Size of the circular buffers
*/
explicit Stats(RenderContext &render_context, size_t buffer_size = 16);
/**
* @brief Destroys the Stats object
*/
~Stats();
/**
* @brief Request specific set of stats to be collected
* @param requested_stats Set of stats to be collected if available
* @param sampling_config Sampling mode configuration (polling or continuous)
*/
void request_stats(const std::set<StatIndex> &requested_stats,
CounterSamplingConfig sampling_config = {CounterSamplingMode::Polling});
/**
* @brief Resizes the stats buffers according to the width of the screen
* @param width The width of the screen
*/
void resize(size_t width);
/**
* @brief Checks if an enabled stat is available in the current platform
* @param index The stat index
* @return True if the stat is available, false otherwise
*/
bool is_available(StatIndex index) const;
/**
* @brief Returns data relevant for graphing a specific statistic
* @param index The stat index of the data requested
* @return The data of the specified stat
*/
const StatGraphData &get_graph_data(StatIndex index) const;
/**
* @brief Returns the collected data for a specific statistic
* @param index The stat index of the data requested
* @return The data of the specified stat
*/
const std::vector<float> &get_data(StatIndex index) const
{
return counters.at(index);
};
/**
* @return The requested stats
*/
const std::set<StatIndex> &get_requested_stats() const
{
return requested_stats;
}
/**
* @brief Update statistics, must be called after every frame
* @param delta_time Time since last update
*/
void update(float delta_time);
/**
* @brief A command buffer that we want to collect stats about has just begun
*
* Some stats providers (like the Vulkan extension one) can only collect stats
* about the execution of a specific command buffer. In those cases we need to
* know when a command buffer has begun and when it's about to end so that we
* can inject some extra commands into the command buffer to control the stats
* collection. This method tells the stats provider that a command buffer has
* begun so that can happen. The command buffer must be in a recording state
* when this method is called.
* @param cb The command buffer
*/
void begin_sampling(vkb::core::CommandBufferC &cb);
/**
* @brief A command buffer that we want to collect stats about is about to be ended
*
* Some stats providers (like the Vulkan extension one) can only collect stats
* about the execution of a specific command buffer. In those cases we need to
* know when a command buffer has begun and when it's about to end so that we
* can inject some extra commands into the command buffer to control the stats
* collection. This method tells the stats provider that a command buffer is
* about to be ended so that can happen. The command buffer must be in a recording
* state when this method is called.
* @param cb The command buffer
*/
void end_sampling(vkb::core::CommandBufferC &cb);
private:
/// The render context
RenderContext &render_context;
/// Stats that were requested - they may not all be available
std::set<StatIndex> requested_stats;
/// Provider that tracks frame times
StatsProvider *frame_time_provider;
/// A list of stats providers to use in priority order
std::vector<std::unique_ptr<StatsProvider>> providers;
/// Counter sampling configuration
CounterSamplingConfig sampling_config;
/// Size of the circular buffers
size_t buffer_size;
/// Timer used in the main thread to compute delta time
Timer main_timer;
/// Timer used by the worker thread to throttle counter sampling
Timer worker_timer;
/// Alpha smoothing for running average
float alpha_smoothing{0.2f};
/// Circular buffers for counter data
std::map<StatIndex, std::vector<float>> counters{};
/// Worker thread for continuous sampling
std::thread worker_thread;
/// Promise to stop the worker thread
std::unique_ptr<std::promise<void>> stop_worker;
/// A mutex for accessing measurements during continuous sampling
std::mutex continuous_sampling_mutex;
/// The samples read during continuous sampling
std::vector<StatsProvider::Counters> continuous_samples;
/// A flag specifying if the worker thread should add entries to continuous_samples
bool should_add_to_continuous_samples{false};
/// The samples waiting to be displayed
std::vector<StatsProvider::Counters> pending_samples;
/// A value which helps keep a steady pace of continuous samples output.
float fractional_pending_samples{0.0f};
/// The worker thread function for continuous sampling;
/// it adds a new entry to continuous_samples at every interval
void continuous_sampling_worker(std::future<void> should_terminate);
/// Updates circular buffers for CPU and GPU counters
void push_sample(const StatsProvider::Counters &sample);
// Push counters to external profilers
void profile_counters() const;
};
} // namespace vkb
+137
View File
@@ -0,0 +1,137 @@
/* Copyright (c) 2018-2022, Arm Limited and Contributors
* Copyright (c) 2020-2022, Broadcom Inc.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <chrono>
#include <string>
#if defined(VK_USE_PLATFORM_XLIB_KHR)
# undef None
#endif
namespace vkb
{
/**
* @brief Handles of stats to be optionally enabled in @ref vkb::Stats
*/
enum class StatIndex
{
frame_times,
cpu_cycles,
cpu_instructions,
cpu_cache_miss_ratio,
cpu_branch_miss_ratio,
cpu_l1_accesses,
cpu_instr_retired,
cpu_l2_accesses,
cpu_l3_accesses,
cpu_bus_reads,
cpu_bus_writes,
cpu_mem_reads,
cpu_mem_writes,
cpu_ase_spec,
cpu_vfp_spec,
cpu_crypto_spec,
gpu_cycles,
gpu_vertex_cycles,
gpu_load_store_cycles,
gpu_tiles,
gpu_killed_tiles,
gpu_fragment_jobs,
gpu_fragment_cycles,
gpu_ext_reads,
gpu_ext_writes,
gpu_ext_read_stalls,
gpu_ext_write_stalls,
gpu_ext_read_bytes,
gpu_ext_write_bytes,
gpu_tex_cycles,
};
struct StatIndexHash
{
template <typename T>
std::size_t operator()(T t) const
{
return static_cast<std::size_t>(t);
}
};
enum class StatScaling
{
// The stat is not scaled
None,
// The stat is scaled by delta time, useful for per-second values
ByDeltaTime,
// The stat is scaled by another counter, useful for ratios
ByCounter
};
enum class CounterSamplingMode
{
/// Sample counters only when calling update()
Polling,
/// Sample counters continuously, update circular buffers when calling update()
Continuous
};
struct CounterSamplingConfig
{
/// Sampling mode (polling or continuous)
CounterSamplingMode mode;
/// Sampling interval in continuous mode
std::chrono::milliseconds interval{1};
/// Speed of circular buffer updates in continuous mode;
/// at speed = 1.0f a new sample is displayed over 1 second.
float speed{0.5f};
};
// Per-statistic graph data
class StatGraphData
{
public:
/**
* @brief Constructs data for the graph
* @param name Name of the Stat
* @param format Format of the label
* @param scale_factor Any scaling to apply to the data
* @param has_fixed_max Whether the data should have a fixed max value
* @param max_value The maximum value to use
*/
StatGraphData(const std::string &name,
const std::string &format,
float scale_factor = 1.0f,
bool has_fixed_max = false,
float max_value = 0.0f);
StatGraphData() = default;
std::string name;
std::string format;
float scale_factor;
bool has_fixed_max;
float max_value;
};
} // namespace vkb
+66
View File
@@ -0,0 +1,66 @@
/* Copyright (c) 2020-2023, Broadcom Inc. and Contributors
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "stats_provider.h"
namespace vkb
{
// Default graphing values for stats. May be overridden by individual providers.
std::map<StatIndex, StatGraphData> StatsProvider::default_graph_map{
// clang-format off
// StatIndex Name shown in graph Format Scale Fixed_max Max_value
{StatIndex::frame_times, {"Frame Times", "{:3.1f} ms", 1000.0f}},
{StatIndex::cpu_cycles, {"CPU Cycles", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_instructions, {"CPU Instructions", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_cache_miss_ratio, {"Cache Miss Ratio", "{:3.1f}%", 100.0f, true, 100.0f}},
{StatIndex::cpu_branch_miss_ratio, {"Branch Miss Ratio", "{:3.1f}%", 100.0f, true, 100.0f}},
{StatIndex::cpu_l1_accesses, {"CPU L1 Accesses", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_instr_retired, {"CPU Instructions Retired", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_l2_accesses, {"CPU L2 Accesses", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_l3_accesses, {"CPU L3 Accesses", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_bus_reads, {"CPU Bus Read Beats", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_bus_writes, {"CPU Bus Write Beats", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_mem_reads, {"CPU Memory Read Instructions", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_mem_writes, {"CPU Memory Write Instructions", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_ase_spec, {"CPU Speculatively Exec. SIMD Instructions", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_vfp_spec, {"CPU Speculatively Exec. FP Instructions", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::cpu_crypto_spec, {"CPU Speculatively Exec. Crypto Instructions", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::gpu_cycles, {"GPU Cycles", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::gpu_vertex_cycles, {"Vertex Cycles", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::gpu_load_store_cycles, {"Load Store Cycles", "{:4.0f} k/s", static_cast<float>(1e-6)}},
{StatIndex::gpu_tiles, {"Tiles", "{:4.1f} k/s", static_cast<float>(1e-3)}},
{StatIndex::gpu_killed_tiles, {"Tiles killed by CRC match", "{:4.1f} k/s", static_cast<float>(1e-3)}},
{StatIndex::gpu_fragment_jobs, {"Fragment Jobs", "{:4.0f}/s"}},
{StatIndex::gpu_fragment_cycles, {"Fragment Cycles", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::gpu_tex_cycles, {"Shader Texture Cycles", "{:4.0f} k/s", static_cast<float>(1e-3)}},
{StatIndex::gpu_ext_reads, {"External Reads", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::gpu_ext_writes, {"External Writes", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::gpu_ext_read_stalls, {"External Read Stalls", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::gpu_ext_write_stalls, {"External Write Stalls", "{:4.1f} M/s", static_cast<float>(1e-6)}},
{StatIndex::gpu_ext_read_bytes, {"External Read Bytes", "{:4.1f} MiB/s", 1.0f / (1024.0f * 1024.0f)}},
{StatIndex::gpu_ext_write_bytes, {"External Write Bytes", "{:4.1f} MiB/s", 1.0f / (1024.0f * 1024.0f)}},
// clang-format on
};
// Static
const StatGraphData &StatsProvider::default_graph_data(StatIndex index)
{
return default_graph_map.at(index);
}
} // namespace vkb
+105
View File
@@ -0,0 +1,105 @@
/* Copyright (c) 2020-2025, Broadcom Inc. and Contributors
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "common/vk_common.h"
#include "stats_common.h"
namespace vkb
{
namespace core
{
template <vkb::BindingType bindingType>
class CommandBuffer;
using CommandBufferC = CommandBuffer<vkb::BindingType::C>;
} // namespace core
/**
* @brief Abstract interface for all StatsProvider classes
*/
class StatsProvider
{
public:
struct Counter
{
double result;
};
using Counters = std::unordered_map<StatIndex, Counter, StatIndexHash>;
/**
* @brief Virtual Destructor
*/
virtual ~StatsProvider()
{}
/**
* @brief Checks if this provider can supply the given enabled stat
* @param index The stat index
* @return True if the stat is available, false otherwise
*/
virtual bool is_available(StatIndex index) const = 0;
/**
* @brief Retrieve graphing data for the given enabled stat
* @param index The stat index
*/
virtual const StatGraphData &get_graph_data(StatIndex index) const
{
return default_graph_map.at(index);
}
/**
* @brief Retrieve default graphing data for the given stat
* @param index The stat index
*/
static const StatGraphData &default_graph_data(StatIndex index);
/**
* @brief Retrieve a new sample set
* @param delta_time Time since last sample
*/
virtual Counters sample(float delta_time) = 0;
/**
* @brief Retrieve a new sample set from continuous sampling
* @param delta_time Time since last sample
*/
virtual Counters continuous_sample(float delta_time)
{
return Counters();
}
/**
* @brief A command buffer that we want stats about has just begun
* @param cb The command buffer
*/
virtual void begin_sampling(vkb::core::CommandBufferC &cb)
{}
/**
* @brief A command buffer that we want stats about is about to be ended
* @param cb The command buffer
*/
virtual void end_sampling(vkb::core::CommandBufferC &cb)
{}
protected:
static std::map<StatIndex, StatGraphData> default_graph_map;
};
} // namespace vkb
+504
View File
@@ -0,0 +1,504 @@
/* Copyright (c) 2020-2025, Broadcom Inc. and Contributors
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "stats/vulkan_stats_provider.h"
#include "core/command_buffer.h"
#include "core/device.h"
#include "rendering/render_context.h"
#include <regex>
namespace vkb
{
VulkanStatsProvider::VulkanStatsProvider(std::set<StatIndex> &requested_stats,
const CounterSamplingConfig &sampling_config,
RenderContext &render_context) :
render_context(render_context)
{
// Check all the Vulkan capabilities we require are present
if (!is_supported(sampling_config))
{
return;
}
vkb::core::DeviceC &device = render_context.get_device();
const PhysicalDevice &gpu = device.get_gpu();
has_timestamps = gpu.get_properties().limits.timestampComputeAndGraphics;
timestamp_period = gpu.get_properties().limits.timestampPeriod;
// Interrogate device for supported stats
uint32_t queue_family_index = vkb::get_queue_family_index(gpu.get_queue_family_properties(), VK_QUEUE_GRAPHICS_BIT);
// Query number of available counters
uint32_t count = 0;
gpu.enumerate_queue_family_performance_query_counters(queue_family_index, &count,
nullptr, nullptr);
if (count == 0)
{
return; // No counters available
}
std::vector<VkPerformanceCounterKHR> counters(count);
std::vector<VkPerformanceCounterDescriptionKHR> descs(count);
for (uint32_t i = 0; i < count; i++)
{
counters[i].sType = VK_STRUCTURE_TYPE_PERFORMANCE_COUNTER_KHR;
counters[i].pNext = nullptr;
descs[i].sType = VK_STRUCTURE_TYPE_PERFORMANCE_COUNTER_DESCRIPTION_KHR;
descs[i].pNext = nullptr;
}
// Now get the list of counters and their descriptions
gpu.enumerate_queue_family_performance_query_counters(queue_family_index, &count,
counters.data(), descs.data());
// Every vendor has a different set of performance counters each
// with different names. Match them to the stats we want, where available.
if (!fill_vendor_data())
{
return;
}
bool performance_impact = false;
// Now build stat_data by matching vendor_data to Vulkan counter data
for (auto &s : vendor_data)
{
StatIndex index = s.first;
if (requested_stats.find(index) == requested_stats.end())
{
continue; // We weren't asked for this stat
}
VendorStat &init = s.second;
bool found_ctr = false;
bool found_div = (init.divisor_name == "");
uint32_t ctr_idx, div_idx;
std::regex name_regex(init.name);
std::regex div_regex(init.divisor_name);
for (uint32_t i = 0; !(found_ctr && found_div) && i < descs.size(); i++)
{
if (!found_ctr && std::regex_match(descs[i].name, name_regex))
{
ctr_idx = i;
found_ctr = true;
}
if (!found_div && std::regex_match(descs[i].name, div_regex))
{
div_idx = i;
found_div = true;
}
}
if (found_ctr && found_div)
{
if ((descs[ctr_idx].flags & VK_PERFORMANCE_COUNTER_DESCRIPTION_PERFORMANCE_IMPACTING_KHR) ||
(init.divisor_name != "" && descs[div_idx].flags != VK_PERFORMANCE_COUNTER_DESCRIPTION_PERFORMANCE_IMPACTING_KHR))
{
performance_impact = true;
}
// Record the counter data
counter_indices.emplace_back(ctr_idx);
if (init.divisor_name == "")
{
stat_data[index] = StatData(ctr_idx, counters[ctr_idx].storage);
}
else
{
counter_indices.emplace_back(div_idx);
stat_data[index] = StatData(ctr_idx, counters[ctr_idx].storage, init.scaling,
div_idx, counters[div_idx].storage);
}
}
}
if (performance_impact)
LOGW("The collection of performance counters may impact performance");
if (counter_indices.size() == 0)
{
return; // No stats available
}
// Acquire the profiling lock, without which we can't collect stats
VkAcquireProfilingLockInfoKHR info{};
info.sType = VK_STRUCTURE_TYPE_ACQUIRE_PROFILING_LOCK_INFO_KHR;
info.timeout = 2000000000; // 2 seconds (in ns)
if (vkAcquireProfilingLockKHR(device.get_handle(), &info) != VK_SUCCESS)
{
stat_data.clear();
counter_indices.clear();
LOGW("Profiling lock acquisition timed-out");
return;
}
// Now we know the counters and that we can collect them, make a query pool for the results.
if (!create_query_pools(queue_family_index))
{
stat_data.clear();
counter_indices.clear();
return;
}
// These stats are fully supported by this provider and in a single pass, so remove
// from the requested set.
// Subsequent providers will then only look for things that aren't already supported.
for (const auto &s : stat_data)
{
requested_stats.erase(s.first);
}
}
VulkanStatsProvider::~VulkanStatsProvider()
{
if (stat_data.size() > 0)
{
// Release profiling lock
vkReleaseProfilingLockKHR(render_context.get_device().get_handle());
}
}
bool VulkanStatsProvider::fill_vendor_data()
{
const auto &pd_props = render_context.get_device().get_gpu().get_properties();
if (pd_props.vendorID == 0x14E4) // Broadcom devices
{
LOGI("Using Vulkan performance counters from Broadcom device");
// NOTE: The names here are actually regular-expressions.
// Counter names can change between hardware variants for the same vendor,
// so regular expression names mean that multiple h/w variants can be easily supported.
// clang-format off
vendor_data = {
{StatIndex::gpu_cycles, {"cycle_count"}},
{StatIndex::gpu_vertex_cycles, {"gpu_vertex_cycles"}},
{StatIndex::gpu_fragment_cycles, {"gpu_fragment_cycles"}},
{StatIndex::gpu_fragment_jobs, {"render_jobs_completed"}},
{StatIndex::gpu_ext_reads, {"gpu_mem_reads"}},
{StatIndex::gpu_ext_writes, {"gpu_mem_writes"}},
{StatIndex::gpu_ext_read_bytes, {"gpu_bytes_read"}},
{StatIndex::gpu_ext_write_bytes, {"gpu_bytes_written"}},
};
// clang-format on
// Override vendor-specific graph data
vendor_data.at(StatIndex::gpu_vertex_cycles).set_vendor_graph_data({"Vertex/Coord/User Cycles", "{:4.1f} M/s", static_cast<float>(1e-6)});
vendor_data.at(StatIndex::gpu_fragment_jobs).set_vendor_graph_data({"Render Jobs", "{:4.0f}/s"});
return true;
}
#if 0
else if (pd_props.vendorID == xxxx) // Other vendor's devices
{
// Fill vendor_data for other vendor
return true;
}
#endif
{
// Unsupported vendor
return false;
}
}
bool VulkanStatsProvider::create_query_pools(uint32_t queue_family_index)
{
vkb::core::DeviceC &device = render_context.get_device();
const PhysicalDevice &gpu = device.get_gpu();
uint32_t num_framebuffers = static_cast<uint32_t>(render_context.get_render_frames().size());
// Now we know the available counters, we can build a query pool that will collect them.
// We will check that the counters can be collected in a single pass. Multi-pass would
// be a big performance hit so for these samples, we don't want to use it.
VkQueryPoolPerformanceCreateInfoKHR perf_create_info{};
perf_create_info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_PERFORMANCE_CREATE_INFO_KHR;
perf_create_info.queueFamilyIndex = queue_family_index;
perf_create_info.counterIndexCount = static_cast<uint32_t>(counter_indices.size());
perf_create_info.pCounterIndices = counter_indices.data();
uint32_t passes_needed = gpu.get_queue_family_performance_query_passes(&perf_create_info);
if (passes_needed != 1)
{
// Needs more than one pass, remove all our supported stats
LOGW("Requested Vulkan stats require multiple passes, we won't collect them");
return false;
}
// We will need a query pool to report the stats back to us
VkQueryPoolCreateInfo pool_create_info{};
pool_create_info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
pool_create_info.pNext = &perf_create_info;
pool_create_info.queryType = VK_QUERY_TYPE_PERFORMANCE_QUERY_KHR;
pool_create_info.queryCount = num_framebuffers;
query_pool = std::make_unique<QueryPool>(device, pool_create_info);
if (!query_pool)
{
LOGW("Failed to create performance query pool");
return false;
}
// Reset the query pool before first use. We cannot do these in the command buffer
// as that is invalid usage for performance queries due to the potential for multiple
// passes being required.
query_pool->host_reset(0, num_framebuffers);
if (has_timestamps)
{
// If we support timestamp queries we will use those to more accurately measure
// the time spent executing a command buffer than just a frame-to-frame timer
// in software.
VkQueryPoolCreateInfo timestamp_pool_create_info{};
timestamp_pool_create_info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
timestamp_pool_create_info.queryType = VK_QUERY_TYPE_TIMESTAMP;
timestamp_pool_create_info.queryCount = num_framebuffers * 2; // 2 timestamps per frame (start & end)
timestamp_pool = std::make_unique<QueryPool>(device, timestamp_pool_create_info);
}
return true;
}
bool VulkanStatsProvider::is_supported(const CounterSamplingConfig &sampling_config) const
{
// Continuous sampling mode cannot be supported by VK_KHR_performance_query
if (sampling_config.mode == CounterSamplingMode::Continuous)
{
return false;
}
vkb::core::DeviceC &device = render_context.get_device();
// The VK_KHR_performance_query must be available and enabled
if (!(device.is_extension_enabled("VK_KHR_performance_query") && device.is_extension_enabled("VK_EXT_host_query_reset")))
{
return false;
}
// Check the performance query features flag.
// Note: VK_KHR_get_physical_device_properties2 is a pre-requisite of VK_KHR_performance_query
// so must be present.
VkPhysicalDevicePerformanceQueryFeaturesKHR perf_query_features{};
perf_query_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PERFORMANCE_QUERY_FEATURES_KHR;
VkPhysicalDeviceFeatures2KHR device_features{};
device_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2_KHR;
device_features.pNext = &perf_query_features;
vkGetPhysicalDeviceFeatures2KHR(device.get_gpu().get_handle(), &device_features);
if (!perf_query_features.performanceCounterQueryPools)
{
return false;
}
return true;
}
bool VulkanStatsProvider::is_available(StatIndex index) const
{
return stat_data.find(index) != stat_data.end();
}
const StatGraphData &VulkanStatsProvider::get_graph_data(StatIndex index) const
{
assert(is_available(index) && "VulkanStatsProvider::get_graph_data() called with invalid StatIndex");
const auto &data = vendor_data.find(index)->second;
if (data.has_vendor_graph_data)
{
return data.graph_data;
}
return default_graph_map[index];
}
void VulkanStatsProvider::begin_sampling(vkb::core::CommandBufferC &cb)
{
uint32_t active_frame_idx = render_context.get_active_frame_index();
if (timestamp_pool)
{
// We use TimestampQueries when available to provide a more accurate delta_time.
// This counters are from a single command buffer execution, but the passed
// delta time is a frame-to-frame s/w measure. A timestamp query in the the cmd
// buffer gives the actual elapsed time where the counters were measured.
cb.reset_query_pool(*timestamp_pool, active_frame_idx * 2, 1);
cb.write_timestamp(VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, *timestamp_pool,
active_frame_idx * 2);
}
if (query_pool)
{
cb.begin_query(*query_pool, active_frame_idx, static_cast<VkQueryControlFlags>(0));
}
}
void VulkanStatsProvider::end_sampling(vkb::core::CommandBufferC &cb)
{
uint32_t active_frame_idx = render_context.get_active_frame_index();
if (query_pool)
{
// Perform a barrier to ensure all previous commands complete before ending the query
// This does not block later commands from executing as we use BOTTOM_OF_PIPE in the
// dst stage mask
vkCmdPipelineBarrier(cb.get_handle(),
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
0, 0, nullptr, 0, nullptr, 0, nullptr);
cb.end_query(*query_pool, active_frame_idx);
++queries_ready;
}
if (timestamp_pool)
{
cb.reset_query_pool(*timestamp_pool, active_frame_idx * 2 + 1, 1);
cb.write_timestamp(VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, *timestamp_pool,
active_frame_idx * 2 + 1);
}
}
static double get_counter_value(const VkPerformanceCounterResultKHR &result,
VkPerformanceCounterStorageKHR storage)
{
switch (storage)
{
case VK_PERFORMANCE_COUNTER_STORAGE_INT32_KHR:
return static_cast<double>(result.int32);
case VK_PERFORMANCE_COUNTER_STORAGE_INT64_KHR:
return static_cast<double>(result.int64);
case VK_PERFORMANCE_COUNTER_STORAGE_UINT32_KHR:
return static_cast<double>(result.uint32);
case VK_PERFORMANCE_COUNTER_STORAGE_UINT64_KHR:
return static_cast<double>(result.uint64);
case VK_PERFORMANCE_COUNTER_STORAGE_FLOAT32_KHR:
return static_cast<double>(result.float32);
case VK_PERFORMANCE_COUNTER_STORAGE_FLOAT64_KHR:
return (result.float64);
default:
assert(0);
return 0.0;
}
}
float VulkanStatsProvider::get_best_delta_time(float sw_delta_time) const
{
if (!timestamp_pool)
{
return sw_delta_time;
}
float delta_time = sw_delta_time;
// Query the timestamps to get an accurate delta time
std::array<uint64_t, 2> timestamps;
uint32_t active_frame_idx = render_context.get_active_frame_index();
VkResult r = timestamp_pool->get_results(active_frame_idx * 2, 2,
timestamps.size() * sizeof(uint64_t),
timestamps.data(), sizeof(uint64_t),
VK_QUERY_RESULT_WAIT_BIT | VK_QUERY_RESULT_64_BIT);
if (r == VK_SUCCESS)
{
float elapsed_ns = timestamp_period * static_cast<float>(timestamps[1] - timestamps[0]);
delta_time = elapsed_ns * 0.000000001f;
}
return delta_time;
}
StatsProvider::Counters VulkanStatsProvider::sample(float delta_time)
{
Counters out;
if (!query_pool || queries_ready == 0)
{
return out;
}
uint32_t active_frame_idx = render_context.get_active_frame_index();
VkDeviceSize stride = sizeof(VkPerformanceCounterResultKHR) * counter_indices.size();
std::vector<VkPerformanceCounterResultKHR> results(counter_indices.size());
VkResult r = query_pool->get_results(active_frame_idx, 1,
results.size() * sizeof(VkPerformanceCounterResultKHR),
results.data(), stride, VK_QUERY_RESULT_WAIT_BIT);
if (r != VK_SUCCESS)
{
return out;
}
// Use timestamps to get a more accurate delta if available
delta_time = get_best_delta_time(delta_time);
// Parse the results - they are in the order we gave in counter_indices
for (const auto &s : stat_data)
{
StatIndex si = s.first;
bool need_divisor = (stat_data[si].scaling == StatScaling::ByCounter);
double divisor_value = 1.0;
double value = 0.0;
bool found_ctr = false, found_div = !need_divisor;
for (uint32_t i = 0; !(found_ctr && found_div) && i < counter_indices.size(); i++)
{
if (s.second.counter_index == counter_indices[i])
{
value = get_counter_value(results[i], stat_data[si].storage);
found_ctr = true;
}
if (need_divisor && s.second.divisor_counter_index == counter_indices[i])
{
divisor_value = get_counter_value(results[i], stat_data[si].divisor_storage);
found_div = true;
}
}
if (found_ctr && found_div)
{
if (stat_data[si].scaling == StatScaling::ByDeltaTime && delta_time != 0.0)
{
value /= delta_time;
}
else if (stat_data[si].scaling == StatScaling::ByCounter && divisor_value != 0.0)
{
value /= divisor_value;
}
out[si].result = value;
}
}
// Now reset the query we just fetched the results from
query_pool->host_reset(active_frame_idx, 1);
--queries_ready;
return out;
}
} // namespace vkb
+163
View File
@@ -0,0 +1,163 @@
/* Copyright (c) 2020-2025, Broadcom Inc. and Contributors
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "core/query_pool.h"
#include "stats_provider.h"
namespace vkb
{
class RenderContext;
class VulkanStatsProvider : public StatsProvider
{
private:
struct StatData
{
StatScaling scaling;
uint32_t counter_index;
uint32_t divisor_counter_index;
VkPerformanceCounterStorageKHR storage;
VkPerformanceCounterStorageKHR divisor_storage;
StatGraphData graph_data;
StatData() = default;
StatData(uint32_t counter_index, VkPerformanceCounterStorageKHR storage,
StatScaling stat_scaling = StatScaling::ByDeltaTime,
uint32_t divisor_index = std::numeric_limits<uint32_t>::max(),
VkPerformanceCounterStorageKHR divisor_storage = VK_PERFORMANCE_COUNTER_STORAGE_FLOAT64_KHR) :
scaling(stat_scaling),
counter_index(counter_index),
divisor_counter_index(divisor_index),
storage(storage),
divisor_storage(divisor_storage)
{}
};
struct VendorStat
{
VendorStat(const std::string &name, const std::string &divisor_name = "") :
name(name),
divisor_name(divisor_name)
{
if (divisor_name != "")
scaling = StatScaling::ByCounter;
}
void set_vendor_graph_data(const StatGraphData &data)
{
has_vendor_graph_data = true;
graph_data = data;
}
std::string name;
StatScaling scaling = StatScaling::ByDeltaTime;
std::string divisor_name;
bool has_vendor_graph_data = false;
StatGraphData graph_data;
};
using StatDataMap = std::unordered_map<StatIndex, StatData, StatIndexHash>;
using VendorStatMap = std::unordered_map<StatIndex, VendorStat, StatIndexHash>;
public:
/**
* @brief Constructs a VulkanStatsProvider
* @param requested_stats Set of stats to be collected. Supported stats will be removed from the set.
* @param sampling_config Sampling mode configuration (polling or continuous)
* @param render_context The render context
*/
VulkanStatsProvider(std::set<StatIndex> &requested_stats, const CounterSamplingConfig &sampling_config,
RenderContext &render_context);
/**
* @brief Destructs a VulkanStatsProvider
*/
~VulkanStatsProvider();
/**
* @brief Checks if this provider can supply the given enabled stat
* @param index The stat index
* @return True if the stat is available, false otherwise
*/
bool is_available(StatIndex index) const override;
/**
* @brief Retrieve graphing data for the given enabled stat
* @param index The stat index
*/
const StatGraphData &get_graph_data(StatIndex index) const override;
/**
* @brief Retrieve a new sample set from polled sampling
* @param delta_time Time since last sample
*/
Counters sample(float delta_time) override;
/**
* @brief A command buffer that we want stats about has just begun
* @param cb The command buffer
*/
void begin_sampling(vkb::core::CommandBufferC &cb) override;
/**
* @brief A command buffer that we want stats about is about to be ended
* @param cb The command buffer
*/
void end_sampling(vkb::core::CommandBufferC &cb) override;
private:
bool is_supported(const CounterSamplingConfig &sampling_config) const;
bool fill_vendor_data();
bool create_query_pools(uint32_t queue_family_index);
float get_best_delta_time(float sw_delta_time) const;
private:
// The render context
RenderContext &render_context;
// The query pool for the performance queries
std::unique_ptr<QueryPool> query_pool;
// Do we support timestamp queries
bool has_timestamps{false};
// The timestamp period
float timestamp_period{1.0f};
// Query pool for timestamps
std::unique_ptr<QueryPool> timestamp_pool;
// Map of vendor specific stat data
VendorStatMap vendor_data;
// Only stats which are available and were requested end up in stat_data
StatDataMap stat_data;
// An ordered list of the Vulkan counter ids
std::vector<uint32_t> counter_indices;
// How many queries have been ended?
uint32_t queries_ready = 0;
};
} // namespace vkb