From e2ac8f79730584ea7562ffaa9cf03eb1a810a9ed Mon Sep 17 00:00:00 2001 From: Bruno Da Silva Date: Sat, 6 Jun 2026 21:27:35 +0000 Subject: [PATCH 1/2] fix: add opengl drift correction for gpu zones --- CMakeLists.txt | 1 + manual/tracy.tex | 6 ++++ public/tracy/TracyOpenGL.hpp | 61 ++++++++++++++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b6c1d148..cfc6f028 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,6 +137,7 @@ set_option(TRACY_SYMBOL_OFFLINE_RESOLVE "Instead of full runtime symbol resoluti set_option(TRACY_LIBBACKTRACE_ELF_DYNLOAD_SUPPORT "Enable libbacktrace to support dynamically loaded elfs in symbol resolution resolution after the first symbol resolve operation" OFF TracyClient) set_option(TRACY_DEBUGINFOD "Enable debuginfod support" OFF TracyClient) set_option(TRACY_IGNORE_MEMORY_FAULTS "Ignore instrumentation errors from memory free events that do not have a matching allocation" OFF TracyClient) +set_option(TRACY_OPENGL_AUTO_CALIBRATION "Periodically recalibrate OpenGL GPU/CPU clock drift (forces a CPU/GPU sync each time)" OFF TracyClient) # advanced set_option(TRACY_VERBOSE "[advanced] Verbose output from the profiler" OFF TracyClient) diff --git a/manual/tracy.tex b/manual/tracy.tex index bbd27451..e1310ae9 100644 --- a/manual/tracy.tex +++ b/manual/tracy.tex @@ -1701,6 +1701,12 @@ logo=\bcattention \end{itemize} \end{bclogo} +\subparagraph{Calibrated context} + +By default, the OpenGL context is uncalibrated: the CPU and GPU clocks are aligned only once, when the context is created, so over long captures the two time domains may drift apart (section~\ref{options} describes correcting this drift manually). Defining \texttt{TRACY\_OPENGL\_AUTO\_CALIBRATION} before including \texttt{TracyOpenGL.hpp} enables periodic recalibration instead: roughly once per second Tracy samples the GPU and CPU clocks together and emits a calibration event, allowing the profiler to track and remove the drift automatically. + +This is opt-in because OpenGL exposes no atomic CPU+GPU timestamp query (unlike Vulkan's \texttt{VK\_EXT\_calibrated\_timestamps} or Direct3D~12, whose contexts are always calibrated). Recalibration therefore reads the GPU clock with \texttt{glGetInteger64v(GL\_TIMESTAMP)}, which forces a CPU/GPU synchronization (a pipeline stall) each time it runs. Enable it only when the improved long-capture alignment is worth the periodic stall. + \subsubsection{Vulkan} Similarly, for Vulkan support you should include the \texttt{public/tracy/TracyVulkan.hpp} header file. Tracing Vulkan devices and queues is a bit more involved, and the Vulkan initialization macro \texttt{TracyVkContext(physdev, device, queue, cmdbuf)} returns an instance of \texttt{TracyVkCtx} object, which tracks an associated Vulkan queue. Cleanup is performed using the \texttt{TracyVkDestroy(ctx)} macro. You may create multiple Vulkan contexts. To set a custom name for the context, use the \texttt{TracyVkContextName(ctx, name, size)} macro. diff --git a/public/tracy/TracyOpenGL.hpp b/public/tracy/TracyOpenGL.hpp index 20554217..f9e35b15 100644 --- a/public/tracy/TracyOpenGL.hpp +++ b/public/tracy/TracyOpenGL.hpp @@ -34,6 +34,9 @@ public: #include #include #include +#ifdef TRACY_OPENGL_AUTO_CALIBRATION +# include +#endif #include "Tracy.hpp" #include "../client/TracyProfiler.hpp" @@ -106,6 +109,14 @@ public: GLint bits; glGetQueryiv( GL_TIMESTAMP, GL_QUERY_COUNTER_BITS, &bits ); +#ifdef TRACY_OPENGL_AUTO_CALIBRATION + // The anchor above is never refreshed; advertise calibration and emit periodic + // GpuCalibration events to correct CPU/GPU drift (see Recalibrate). Opt-in, + // because Recalibrate() calls glGetInteger64v( GL_TIMESTAMP ), which forces a + // CPU/GPU sync. + m_prevCalibration = GetHostTimeNs(); +#endif + const float period = 1.f; const auto thread = GetThreadHandle(); TracyLfqPrepare( QueueType::GpuNewContext ); @@ -114,7 +125,11 @@ public: MemWrite( &item->gpuNewContext.thread, thread ); MemWrite( &item->gpuNewContext.period, period ); MemWrite( &item->gpuNewContext.context, m_context ); +#ifdef TRACY_OPENGL_AUTO_CALIBRATION + MemWrite( &item->gpuNewContext.flags, GpuContextFlags( GpuContextCalibration ) ); +#else MemWrite( &item->gpuNewContext.flags, GpuContextFlags( 0 ) ); +#endif MemWrite( &item->gpuNewContext.type, GpuContextType::OpenGl ); #ifdef TRACY_ON_DEMAND @@ -143,8 +158,6 @@ public: { ZoneScopedC( Color::Red4 ); - if( m_tail == m_head ) return; - #ifdef TRACY_ON_DEMAND if( !GetProfiler().IsConnected() ) { @@ -153,6 +166,14 @@ public: } #endif +#ifdef TRACY_OPENGL_AUTO_CALIBRATION + // Before the drain's early-returns, so it runs even on frames with no + // completed queries. + Recalibrate(); +#endif + + if( m_tail == m_head ) return; + while( m_tail != m_head ) { GLint available; @@ -173,6 +194,38 @@ public: } private: +#ifdef TRACY_OPENGL_AUTO_CALIBRATION + // Monotonic host ns for the inter-calibration interval (cpuDelta), kept + // separate from Profiler::GetTime() as in the D3D12/Vulkan backends. + static tracy_force_inline int64_t GetHostTimeNs() + { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch() ).count(); + } + + // OpenGL has no atomic CPU+GPU timestamp query, so sample back-to-back; the + // gap is negligible against the recalibration interval below. Note this forces + // a CPU/GPU sync, which is why the whole path is opt-in (TRACY_OPENGL_AUTO_CALIBRATION). + tracy_force_inline void Recalibrate() + { + const int64_t hostNow = GetHostTimeNs(); + const int64_t delta = hostNow - m_prevCalibration; + if( delta < 1000ll * 1000 * 1000 ) return; // throttle: ~once per second + + int64_t tgpu; + glGetInteger64v( GL_TIMESTAMP, &tgpu ); + const int64_t refCpu = Profiler::GetTime(); + m_prevCalibration = hostNow; + + TracyLfqPrepare( QueueType::GpuCalibration ); + MemWrite( &item->gpuCalibration.gpuTime, tgpu ); + MemWrite( &item->gpuCalibration.cpuTime, refCpu ); + MemWrite( &item->gpuCalibration.cpuDelta, delta ); + MemWrite( &item->gpuCalibration.context, m_context ); + TracyLfqCommit; + } +#endif + tracy_force_inline unsigned int NextQueryId() { const auto id = m_head; @@ -196,6 +249,10 @@ private: unsigned int m_head; unsigned int m_tail; + +#ifdef TRACY_OPENGL_AUTO_CALIBRATION + int64_t m_prevCalibration; // host-ns timestamp of the last emitted calibration +#endif }; class GpuCtxScope From fc4f52e61d7ab545a1624bae679199540aedb545 Mon Sep 17 00:00:00 2001 From: Bruno Da Silva Date: Sun, 7 Jun 2026 20:14:36 +0000 Subject: [PATCH 2/2] add opengl drift correction option to meson.options/meson.build --- meson.build | 4 ++++ meson.options | 1 + 2 files changed, 5 insertions(+) diff --git a/meson.build b/meson.build index 4afb61f7..56cfbed1 100644 --- a/meson.build +++ b/meson.build @@ -135,6 +135,10 @@ if get_option('ignore_memory_faults') tracy_common_args += ['-DTRACY_IGNORE_MEMORY_FAULTS'] endif +if get_option('opengl_auto_calibration') + tracy_common_args += ['-DTRACY_OPENGL_AUTO_CALIBRATION'] +endif + tracy_shared_libs = get_option('default_library') == 'shared' if tracy_shared_libs diff --git a/meson.options b/meson.options index 55e4fbf1..f638d071 100644 --- a/meson.options +++ b/meson.options @@ -29,3 +29,4 @@ option('verbose', type : 'boolean', value : false, description : 'Enable verbose option('no_internal_message', type : 'boolean', value : false, description : 'Prevent the profiler from logging messages') option('debuginfod', type : 'boolean', value : false, description : 'Enable debuginfod support') option('ignore_memory_faults', type : 'boolean', value : false, description : 'Ignore instrumentation errors from memory free events that do not have a matching allocation') +option('opengl_auto_calibration', type : 'boolean', value : false, description : 'Periodically recalibrate OpenGL GPU/CPU clock drift (forces a CPU/GPU sync each time)')