@@ -34,6 +34,7 @@ class GpuCtxScope
3434#include < atomic>
3535#include < assert.h>
3636#include < stdlib.h>
37+ #include < chrono>
3738
3839#include " Tracy.hpp"
3940#include " ../client/TracyProfiler.hpp"
@@ -106,6 +107,13 @@ class GpuCtx
106107 GLint bits;
107108 glGetQueryiv ( GL_TIMESTAMP, GL_QUERY_COUNTER_BITS, &bits );
108109
110+ // Seed periodic recalibration (see Recalibrate). Without this the single
111+ // anchor above is never refreshed and the GPU/CPU clocks drift apart
112+ // (measured ~33 ppm), so GPU zones slide off the CPU timeline over a
113+ // session. Advertising GpuContextCalibration tells the server to expect
114+ // GpuCalibration events and apply rate correction.
115+ m_prevCalibration = GetHostTimeNs ();
116+
109117 const float period = 1 .f ;
110118 const auto thread = GetThreadHandle ();
111119 TracyLfqPrepare ( QueueType::GpuNewContext );
@@ -114,7 +122,7 @@ class GpuCtx
114122 MemWrite ( &item->gpuNewContext .thread , thread );
115123 MemWrite ( &item->gpuNewContext .period , period );
116124 MemWrite ( &item->gpuNewContext .context , m_context );
117- MemWrite ( &item->gpuNewContext .flags , GpuContextFlags ( 0 ) );
125+ MemWrite ( &item->gpuNewContext .flags , GpuContextFlags ( GpuContextCalibration ) );
118126 MemWrite ( &item->gpuNewContext .type , GpuContextType::OpenGl );
119127
120128#ifdef TRACY_ON_DEMAND
@@ -143,8 +151,6 @@ class GpuCtx
143151 {
144152 ZoneScopedC ( Color::Red4 );
145153
146- if ( m_tail == m_head ) return ;
147-
148154#ifdef TRACY_ON_DEMAND
149155 if ( !GetProfiler ().IsConnected () )
150156 {
@@ -153,6 +159,13 @@ class GpuCtx
153159 }
154160#endif
155161
162+ // Emit a fresh clock-pair periodically so the server can correct GPU/CPU
163+ // drift. Done before the (possibly early-returning) query drain so it
164+ // still runs on frames with no completed queries.
165+ Recalibrate ();
166+
167+ if ( m_tail == m_head ) return ;
168+
156169 while ( m_tail != m_head )
157170 {
158171 GLint available;
@@ -173,6 +186,40 @@ class GpuCtx
173186 }
174187
175188private:
189+ // Monotonic host nanoseconds, used only for measuring the interval between
190+ // calibrations (cpuDelta). Independent of Profiler::GetTime() (raw TSC ticks)
191+ // on purpose — matches how the D3D12/Vulkan backends source cpuDelta from a
192+ // separate ns clock while reporting cpuTime from Profiler::GetTime().
193+ static tracy_force_inline int64_t GetHostTimeNs ()
194+ {
195+ return std::chrono::duration_cast<std::chrono::nanoseconds>(
196+ std::chrono::steady_clock::now ().time_since_epoch () ).count ();
197+ }
198+
199+ // Re-anchor the GPU<->CPU mapping. OpenGL has no atomic dual-clock read
200+ // (unlike Vulkan's vkGetCalibratedTimestampsEXT), so we sample GL_TIMESTAMP
201+ // and the host clock back-to-back; the sub-microsecond gap is negligible
202+ // against a >=1s recalibration interval. The server uses (gpuTime, cpuDelta)
203+ // to derive the clock-rate ratio and continuously correct drift.
204+ tracy_force_inline void Recalibrate ()
205+ {
206+ const int64_t hostNow = GetHostTimeNs ();
207+ const int64_t delta = hostNow - m_prevCalibration;
208+ if ( delta < 1000ll * 1000 * 1000 ) return ; // throttle: ~once per second
209+
210+ int64_t tgpu;
211+ glGetInteger64v ( GL_TIMESTAMP, &tgpu );
212+ const int64_t refCpu = Profiler::GetTime ();
213+ m_prevCalibration = hostNow;
214+
215+ TracyLfqPrepare ( QueueType::GpuCalibration );
216+ MemWrite ( &item->gpuCalibration .gpuTime , tgpu );
217+ MemWrite ( &item->gpuCalibration .cpuTime , refCpu );
218+ MemWrite ( &item->gpuCalibration .cpuDelta , delta );
219+ MemWrite ( &item->gpuCalibration .context , m_context );
220+ TracyLfqCommit;
221+ }
222+
176223 tracy_force_inline unsigned int NextQueryId ()
177224 {
178225 const auto id = m_head;
@@ -196,6 +243,8 @@ class GpuCtx
196243
197244 unsigned int m_head;
198245 unsigned int m_tail;
246+
247+ int64_t m_prevCalibration; // host-ns timestamp of the last emitted calibration
199248};
200249
201250class GpuCtxScope
0 commit comments