[PATCH v3 0/2] MR9875: Implement InterruptTimeBias and use it in RtlQueryUnbiasedInterruptTime.
After https://gitlab.winehq.org/wine/wine/-/merge_requests/8916 `RtlQueryUnbiasedInterruptTime` now includes suspend time as well when it shouldn't. This fixes this and also allows other components like winevulkan to ~~use `RtlQueryUnbiasedInterruptTime`, if that kind of tick count is necessary there~~ get the InterruptTimeBias for their own clock conversions. ~~Since calculating the bias is relatively expensive, this is done together with the timezone bias update every second in such a way that it can only monotonically increase.~~ -- v3: server: Calculate and store InterruptTimeBias in user shared data. ntdll: Subtract InterruptTimeBias in RtlQueryUnbiasedInterruptTime(). https://gitlab.winehq.org/wine/wine/-/merge_requests/9875
From: Marc-Aurel Zent <mzent@codeweavers.com> --- dlls/ntdll/time.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dlls/ntdll/time.c b/dlls/ntdll/time.c index 105a6cf5bc8..e417cc9f18f 100644 --- a/dlls/ntdll/time.c +++ b/dlls/ntdll/time.c @@ -463,6 +463,7 @@ NTSTATUS WINAPI RtlSetTimeZoneInformation( const RTL_TIME_ZONE_INFORMATION *tzin BOOL WINAPI RtlQueryUnbiasedInterruptTime(ULONGLONG *time) { ULONG high, low; + ULONGLONG bias; if (!time) { @@ -474,9 +475,9 @@ BOOL WINAPI RtlQueryUnbiasedInterruptTime(ULONGLONG *time) { high = user_shared_data->InterruptTime.High1Time; low = user_shared_data->InterruptTime.LowPart; + bias = user_shared_data->InterruptTimeBias; } while (high != user_shared_data->InterruptTime.High2Time); - /* FIXME: should probably subtract InterruptTimeBias */ - *time = (ULONGLONG)high << 32 | low; + *time = ((ULONGLONG)high << 32 | low) - bias; return TRUE; } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9875
From: Marc-Aurel Zent <mzent@codeweavers.com> --- server/fd.c | 27 ++++++++++++++++++++++++++- server/request.c | 18 +++++++++++------- server/request.h | 4 ++-- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/server/fd.c b/server/fd.c index f70bec354a3..389d7e82143 100644 --- a/server/fd.c +++ b/server/fd.c @@ -351,6 +351,7 @@ static struct list abs_timeout_list = LIST_INIT(abs_timeout_list); /* sorted abs static struct list rel_timeout_list = LIST_INIT(rel_timeout_list); /* sorted relative timeouts list */ timeout_t current_time; timeout_t monotonic_time; +static timeout_t current_suspend_bias; struct _KUSER_SHARED_DATA *user_shared_data = NULL; static const timeout_t user_shared_data_timeout = 16 * 10000; @@ -377,6 +378,15 @@ static void atomic_store_long(volatile LONG *ptr, LONG value) #endif } +static void atomic_store_ulonglong(volatile ULONGLONG *ptr, ULONGLONG value) +{ +#if defined(__i386__) || defined(__x86_64__) + *ptr = value; +#else + __atomic_store_n(ptr, value, __ATOMIC_SEQ_CST); +#endif +} + static void set_user_shared_data_time(void) { timeout_t tick_count = monotonic_time / 10000; @@ -407,6 +417,7 @@ static void set_user_shared_data_time(void) atomic_store_long(&user_shared_data->InterruptTime.High2Time, monotonic_time >> 32); atomic_store_ulong(&user_shared_data->InterruptTime.LowPart, monotonic_time); + atomic_store_ulonglong(&user_shared_data->InterruptTimeBias, current_suspend_bias); atomic_store_long(&user_shared_data->InterruptTime.High1Time, monotonic_time >> 32); atomic_store_long(&user_shared_data->TickCount.High2Time, tick_count >> 32); @@ -419,9 +430,23 @@ void set_current_time(void) { static const timeout_t ticks_1601_to_1970 = (timeout_t)86400 * (369 * 365 + 89) * TICKS_PER_SEC; struct timeval now; + timeout_t bias1, bias2 = 0; + timeout_t bias_difference; gettimeofday( &now, NULL ); current_time = (timeout_t)now.tv_sec * TICKS_PER_SEC + now.tv_usec * 10 + ticks_1601_to_1970; - monotonic_time = monotonic_counter(); + for (;;) + { + monotonic_time = monotonic_counter( &bias1 ); + if (bias1 < current_suspend_bias + 10000) + break; + monotonic_time = monotonic_counter( &bias2 ); + bias_difference = bias1 > bias2 ? bias1 - bias2 : bias2 - bias1; + if (bias_difference < 100) + { + current_suspend_bias = (bias1 + bias2) / 2; + break; + } + } if (user_shared_data) set_user_shared_data_time(); } diff --git a/server/request.c b/server/request.c index 432a5918892..239e93ebf60 100644 --- a/server/request.c +++ b/server/request.c @@ -505,24 +505,28 @@ int send_client_fd( struct process *process, int fd, obj_handle_t handle ) return -1; } -/* return a monotonic time counter */ -timeout_t monotonic_counter(void) +/* return a monotonic time counter and optional suspend bias */ +timeout_t monotonic_counter( timeout_t *bias ) { + timeout_t counter, unbiased_counter = 0; #ifdef __APPLE__ static mach_timebase_info_data_t timebase; if (!timebase.denom) mach_timebase_info( &timebase ); - return mach_continuous_time() * timebase.numer / timebase.denom / 100; + unbiased_counter = mach_absolute_time() * timebase.numer / timebase.denom / 100; + counter = mach_continuous_time() * timebase.numer / timebase.denom / 100; #elif defined(HAVE_CLOCK_GETTIME) struct timespec ts; + if (!clock_gettime( CLOCK_MONOTONIC, &ts )) + counter = unbiased_counter = (timeout_t)ts.tv_sec * TICKS_PER_SEC + ts.tv_nsec / 100; #ifdef CLOCK_BOOTTIME if (!clock_gettime( CLOCK_BOOTTIME, &ts )) - return (timeout_t)ts.tv_sec * TICKS_PER_SEC + ts.tv_nsec / 100; + counter = (timeout_t)ts.tv_sec * TICKS_PER_SEC + ts.tv_nsec / 100; #endif - if (!clock_gettime( CLOCK_MONOTONIC, &ts )) - return (timeout_t)ts.tv_sec * TICKS_PER_SEC + ts.tv_nsec / 100; #endif - return current_time - server_start_time; + if (!unbiased_counter) counter = unbiased_counter = current_time - server_start_time; + if (bias) *bias = counter - unbiased_counter; + return counter; } static void master_socket_dump( struct object *obj, int verbose ) diff --git a/server/request.h b/server/request.h index 13254d967ed..2a082c874b6 100644 --- a/server/request.h +++ b/server/request.h @@ -54,7 +54,7 @@ extern int receive_fd( struct process *process ); extern int send_client_fd( struct process *process, int fd, obj_handle_t handle ); extern void read_request( struct thread *thread ); extern void write_reply( struct thread *thread ); -extern timeout_t monotonic_counter(void); +extern timeout_t monotonic_counter( timeout_t *bias ); extern void open_master_socket(void); extern void close_master_socket( timeout_t timeout ); extern void shutdown_master_socket(void); @@ -69,7 +69,7 @@ extern void trace_reply( enum request req, const union generic_reply *reply ); /* get current tick count to return to client */ static inline unsigned int get_tick_count(void) { - return monotonic_counter() / 10000; + return monotonic_counter( NULL ) / 10000; } /* get the request vararg data */ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9875
If system is suspended / resumed right between these two calls the bias gets completely off.
Indeed, this could happen and is fixed now with v3, where after a jump in bias over the threshold the new bias is recalculated in a calibration loop run to get an accurate reading.
Note as well that this here doesn't prevent other processes from executing and reading stale bias values from USD after system is resumed
The InterruptTimeBias in USD here is meant to be paired with the InterruptTime also in USD (I also sandwiched it between the high and low values to always be in sync with it in v3). This value is as stale as all other tick counts and time data in USD after resume and this did not seem to have caused any problem yet, so I don't think we need any extra measures to make wineserver executes first and/or suspend/resume hooks. As you said though there is a theoretical possible race after suspend if mixed with QPC (which is technically not using this value where it should be used). If the first converted vulkan timestamp after suspend being off in rare cases is a critical issue there, winevulkan could do something like: - get QPC (latest timestamp independent of USD) - get InterruptTimeBias - query InterruptTime (from USD) If the QPC and InterruptTime values are not close (which both include suspend time), then we know that the bias is stale and can just get it again. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9875#note_126899
On Wed Jan 14 08:21:19 2026 +0000, Marc-Aurel Zent wrote: > > If system is suspended / resumed right between these two calls the > bias gets completely off. > Indeed, this could happen and is fixed now with v3, where after a jump > in bias over the threshold the new bias is recalculated in a calibration > loop run to get an accurate reading. > > Note as well that this here doesn't prevent other processes from > executing and reading stale bias values from USD after system is resumed > The InterruptTimeBias in USD here is meant to be paired with the > InterruptTime also in USD (I also sandwiched it between the high and low > values to always be in sync with it in v3). This value is as stale as > all other tick counts and time data in USD after resume and this did not > seem to have caused any problem yet, so I don't think we need any extra > measures to make wineserver executes first and/or suspend/resume hooks. > As you said though there is a theoretical possible race after suspend if > mixed with QPC (which is technically not using this value where it > should be used). If the first converted vulkan timestamp after suspend > being off in rare cases is a critical issue there, winevulkan could do > something like: > - get QPC (latest timestamp independent of USD) > - get InterruptTimeBias > - query InterruptTime (from USD) > If the QPC and InterruptTime values are not close (which both include > suspend time), then we know that the bias is stale and can just get it again. This is still not going to work. Trying to get a more accurate bias by reading it in a loop is inefficient. It also leads to varying bias values over time which could end up with spurious time jumps back and forth in time when read from client side. For the latter, after resuming from suspend you could have client read QPC, read interrupt bias before wineserver has updated it, wineserver gets scheduled and update interrupt bias and time, client reads interrupt time and finds it close enough to QPC, assumes that read bias is good when it's not. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9875#note_126905
On Wed Jan 14 08:33:01 2026 +0000, Rémi Bernon wrote:
This is still not going to work. Trying to get a more accurate bias by reading it in a loop is inefficient. It also leads to varying bias values over time which could end up with spurious time jumps back and forth in time when read from client side. For the latter, after resuming from suspend you could have client read QPC, read interrupt bias before wineserver has updated it, wineserver gets scheduled and update interrupt bias and time, client reads interrupt time and finds it close enough to QPC, assumes that read bias is good when it's not. We can't be jumping back and forth, because the bias is only ever increased, and only past a comparatively large threshold of one millisecond for a measurement that has jitter in the nanosecond range.
For the latter, after resuming from suspend you could have client read QPC, read interrupt bias before wineserver has updated it, wineserver gets scheduled and update interrupt bias and time, client reads interrupt time and finds it close enough to QPC, assumes that read bias is good when it's not.
Reading the bias again after the presumed interrupt time again and comparing it would fix this, but I am not too sure such a complex solution for this case needs to be engineered here. On a side note regarding measuring this in user space and efficiency, the loop never did more than one iteration on any configuration I tested, with a script continually suspending and resuming the system. I originally thought that these are also system calls, but that is not true on modern Linux and macOS, where they are directly read from vDSO and commpage respectively. It is mostly for peace of mind in the unlikely case of suspend right between the biased and unbiased time measurement. Interestingly there are also the private macOS apis `mach_get_times(uint64_t* absolute_time, uint64_t* cont_time, struct timespec *tp)` and `uint64_t _mach_continuous_time_base(void)`, with the former fetching a triplet of all aligned values and the latter being exactly the offset between suspended and unsuspended time. Comparing this implementation to the true value the system uses, I got an error between 0 and 14ns with the current approach. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9875#note_126914
On Wed Jan 14 09:49:18 2026 +0000, Marc-Aurel Zent wrote:
We can't be jumping back and forth, because the bias is only ever increased, and only past a comparatively large threshold of one millisecond for a measurement that has jitter in the nanosecond range.
For the latter, after resuming from suspend you could have client read QPC, read interrupt bias before wineserver has updated it, wineserver gets scheduled and update interrupt bias and time, client reads interrupt time and finds it close enough to QPC, assumes that read bias is good when it's not. Reading the bias again after the presumed interrupt time again and comparing it would fix this, but I am not too sure such a complex solution for this case needs to be engineered here. On a side note regarding measuring this in user space and efficiency, the loop never did more than one iteration on any configuration I tested, with a script continually suspending and resuming the system. I originally thought that these are also system calls, but that is not true on modern Linux and macOS, where they are directly read from vDSO and commpage respectively. It is mostly for peace of mind in the unlikely case of suspend right between the biased and unbiased time measurement. Interestingly there are also the private macOS apis `mach_get_times(uint64_t* absolute_time, uint64_t* cont_time, struct timespec *tp)` and `uint64_t _mach_continuous_time_base(void)`, with the former fetching a triplet of all aligned values and the latter being exactly the offset between suspended and unsuspended time. Comparing this implementation to the true value the system uses, I got an error between 0 and 14ns with the current approach. Well idk, my opinion is that having tried myself to re-implement clock emulation for QPC optimization from the available APIs, I am not confident that this can be done 100% reliably in user space. The devil is in the details and in very unfortunate corner cases that are easy to miss when testing under normal operation and on a single platform.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/9875#note_126916
participants (3)
-
Marc-Aurel Zent -
Marc-Aurel Zent (@mzent) -
Rémi Bernon