Skip to content

Commit 8ef6526

Browse files
committed
Fix GPU timestamp calibration error in Vulkan::QueryPool on Apple with MoltenVK
1. Time domain selection — replaced the hardcoded constant with a platform preference list, resolved at runtime against what the device actually reports. macOS/iOS now get CLOCK_MONOTONIC_RAW, which MoltenVK does support. I verified against the installed MoltenVK with a standalone probe: it advertises DEVICE + CLOCK_MONOTONIC_RAW, the CPU timestamps match clock_gettime(CLOCK_MONOTONIC_RAW) to within 42 ns (so GetQpcToNSecMultiplier() == 1 is correct), timestampPeriod is 1.0 ns, and calibration deviation is 0–958 ns — well inside the retry loop's bounds. Tracy's calibration path only consumes the nanosecond delta, so the profiling data is genuinely correct, not just non-crashing. 2. Query pool sizing — the pool asked for a fixed 32768 queries while its data buffer only holds frame_buffers_count × 1000 timestamps. That over-allocation exceeded Metal's 32 KB MTLCounterSampleBuffer limit, so MoltenVK printed an error per queue and silently fell back to emulated timestamps, defeating the point of a Profile build. Sizing the pool to the buffer's real capacity fixes it; the effective query limit is unchanged, since the data buffer was always the binding constraint.
1 parent f4fb61c commit 8ef6526

2 files changed

Lines changed: 52 additions & 15 deletions

File tree

Modules/Graphics/RHI/Vulkan/Include/Methane/Graphics/Vulkan/QueryPool.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class TimestampQueryPool final
106106
CalibratedTimestamps Calibrate() override;
107107

108108
private:
109+
const vk::TimeDomainEXT m_vk_cpu_time_domain;
109110
uint64_t m_deviation = 0U;
110111
};
111112

Modules/Graphics/RHI/Vulkan/Sources/Methane/Graphics/Vulkan/QueryPool.cpp

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,29 @@ Vulkan GPU query pool implementation.
3333
#include <Methane/Instrumentation.h>
3434
#include <Methane/Checks.hpp>
3535

36+
#include <algorithm>
37+
#include <array>
3638
#include <chrono>
3739
#include <limits>
40+
#include <span>
41+
#include <string_view>
42+
#include <vector>
43+
#include <fmt/ranges.h>
3844
#include <magic_enum/magic_enum.hpp>
3945

40-
static const vk::TimeDomainEXT g_vk_cpu_time_domain =
46+
// CPU time domains which can be used to calibrate GPU timestamps, in the order of preference.
47+
// The selected domain must return timestamps in the units which Data::GetQpcToNSecMultiplier()
48+
// converts to nanoseconds: QueryPerformanceCounter ticks on Windows and nanoseconds of the POSIX
49+
// monotonic clocks on all other platforms (where that multiplier is 1).
50+
static constexpr std::array g_vk_cpu_time_domains{
4151
#if defined(_WIN32)
42-
vk::TimeDomainEXT::eQueryPerformanceCounter;
43-
#elif defined(__linux__) && defined CLOCK_MONOTONIC_RAW
44-
vk::TimeDomainEXT::eClockMonotonicRaw;
52+
vk::TimeDomainEXT::eQueryPerformanceCounter
4553
#else
46-
static_cast<vk::TimeDomainEXT>(-1);
54+
// MoltenVK exposes CLOCK_MONOTONIC_RAW only, while other Vulkan drivers may expose either of these.
55+
vk::TimeDomainEXT::eClockMonotonicRaw,
56+
vk::TimeDomainEXT::eClockMonotonic
4757
#endif
58+
};
4859

4960
namespace Methane::Graphics::Vulkan
5061
{
@@ -70,6 +81,33 @@ static Data::Size GetMaxTimestampsCount(const Rhi::IContext& context, uint32_t m
7081
return frames_count * max_timestamps_per_frame;
7182
}
7283

84+
// Used only to build the description of the failed check below, which is compiled out with METHANE_CHECKS_ENABLED=OFF.
85+
[[maybe_unused]] static std::vector<std::string_view> GetTimeDomainNames(std::span<const vk::TimeDomainEXT> time_domains)
86+
{
87+
META_FUNCTION_TASK();
88+
std::vector<std::string_view> time_domain_names;
89+
time_domain_names.reserve(time_domains.size());
90+
std::ranges::transform(time_domains, std::back_inserter(time_domain_names),
91+
[](vk::TimeDomainEXT time_domain) { return magic_enum::enum_name(time_domain); });
92+
return time_domain_names;
93+
}
94+
95+
static vk::TimeDomainEXT GetCalibrateableCpuTimeDomain(const vk::PhysicalDevice& vk_physical_device)
96+
{
97+
META_FUNCTION_TASK();
98+
// Not every driver exposes every CPU time domain: MoltenVK, for example, exposes CLOCK_MONOTONIC_RAW only,
99+
// so the first domain supported by the device is taken from the platform preference list instead of
100+
// requiring one hard-coded domain to be present.
101+
const std::vector<vk::TimeDomainEXT> calibrateable_time_domains = vk_physical_device.getCalibrateableTimeDomainsEXT();
102+
const auto cpu_time_domain_it = std::ranges::find_first_of(g_vk_cpu_time_domains, calibrateable_time_domains);
103+
const bool is_cpu_time_domain_calibrateable = cpu_time_domain_it != g_vk_cpu_time_domains.end();
104+
META_CHECK_TRUE_DESCR(is_cpu_time_domain_calibrateable,
105+
"Vulkan does not support calibration of any CPU time domain used on this platform ({}), device supports only ({})",
106+
fmt::join(GetTimeDomainNames(g_vk_cpu_time_domains), ", "),
107+
fmt::join(GetTimeDomainNames(calibrateable_time_domains), ", "));
108+
return is_cpu_time_domain_calibrateable ? *cpu_time_domain_it : g_vk_cpu_time_domains.front();
109+
}
110+
73111
Query::Query(Base::QueryPool& buffer, Base::CommandList& command_list, Index index, Range data_range)
74112
: Base::Query(buffer, command_list, index, data_range)
75113
, m_vk_device(GetVulkanQueryPool().GetVulkanContext().GetVulkanDevice().GetNativeDevice())
@@ -131,9 +169,14 @@ CommandQueue& QueryPool::GetVulkanCommandQueue() noexcept
131169
}
132170

133171
TimestampQueryPool::TimestampQueryPool(CommandQueue& command_queue, uint32_t max_timestamps_per_frame)
134-
: QueryPool(command_queue, Type::Timestamp, 1U << 15U, 1U,
172+
// Query pool is created with exactly as many queries as its data buffer can hold timestamps:
173+
// over-allocating queries makes MoltenVK fail to create the backing MTLCounterSampleBuffer
174+
// (limited to 32768 bytes, i.e. 4096 timestamps) and silently fall back to emulated timestamps.
175+
: QueryPool(command_queue, Type::Timestamp,
176+
GetMaxTimestampsCount(command_queue.GetContext(), max_timestamps_per_frame), 1U,
135177
GetMaxTimestampsCount(command_queue.GetContext(), max_timestamps_per_frame) * sizeof(Timestamp),
136178
sizeof(Timestamp))
179+
, m_vk_cpu_time_domain(GetCalibrateableCpuTimeDomain(command_queue.GetVulkanDevice().GetNativePhysicalDevice()))
137180
{
138181
META_FUNCTION_TASK();
139182

@@ -144,15 +187,8 @@ TimestampQueryPool::TimestampQueryPool(CommandQueue& command_queue, uint32_t max
144187
const float gpu_timestamp_period = vk_physical_device.getProperties().limits.timestampPeriod;
145188
SetGpuFrequency(static_cast<Frequency>(gpu_timestamp_period * std::chrono::nanoseconds(1s).count()));
146189

147-
// Check if Vulkan supports CPU time domains calibration
148-
const auto calibrateable_time_domains = vk_physical_device.getCalibrateableTimeDomainsEXT();
149-
bool is_cpu_time_domain_calibrateable = std::ranges::find(calibrateable_time_domains, g_vk_cpu_time_domain) != calibrateable_time_domains.end();
150-
META_CHECK_TRUE_DESCR(is_cpu_time_domain_calibrateable,
151-
"Vulkan does not support calibration of the CPU time domain {}",
152-
magic_enum::enum_name(g_vk_cpu_time_domain));
153-
154190
// Calculate the desired CPU-GPU timestamps deviation
155-
const std::array<vk::CalibratedTimestampInfoEXT, 2> timestamp_infos = {{ { vk::TimeDomainEXT::eDevice }, { g_vk_cpu_time_domain }, }};
191+
const std::array<vk::CalibratedTimestampInfoEXT, 2> timestamp_infos = {{ { vk::TimeDomainEXT::eDevice }, { m_vk_cpu_time_domain }, }};
156192
std::array<uint64_t, 2> timestamps{{}};
157193
std::array<uint64_t, 32> probe_deviations{{}};
158194
for(uint64_t& deviation : probe_deviations)
@@ -189,7 +225,7 @@ Rhi::ITimestampQueryPool::CalibratedTimestamps TimestampQueryPool::Calibrate()
189225
constexpr uint32_t max_calibration_attempts = 16U;
190226

191227
const vk::Device& vk_device = GetVulkanCommandQueue().GetVulkanDevice().GetNativeDevice();
192-
const std::array<vk::CalibratedTimestampInfoEXT, 2> timestamp_infos = {{ { vk::TimeDomainEXT::eDevice }, { g_vk_cpu_time_domain }, }};
228+
const std::array<vk::CalibratedTimestampInfoEXT, 2> timestamp_infos = {{ { vk::TimeDomainEXT::eDevice }, { m_vk_cpu_time_domain }, }};
193229
std::array<uint64_t, 2> timestamps{{}};
194230
std::array<uint64_t, 2> best_timestamps{{}};
195231
uint64_t deviation = 0U;

0 commit comments

Comments
 (0)