-
Notifications
You must be signed in to change notification settings - Fork 1.3k
wineopenxr: Implement Win32 performance counter time conversion functions for MSFS2020 VR Controller Tracking #9347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
I am not a reviewer, but verified this implements the methods. |
|
@GloriousEggroll FYI since this completes the QPC↔XrTime conversion you partially implemented in your GE fork might be worth cherry-picking for GE-Proton |
I don't think you can assume that the XrTime and the performance counter are correlated, which means the current approach is probably not correct. |
|
Performance counters use CLOCK_MONOTONIC_RAW (in proton 10) and they will probably use CLOCK_BOOTTIME (in proton 11, unless they decide to revert that change) so the timespec obtained from the corresponding timespec extension most likely isn't very helpful either since neither of those are CLOCK_MONOTONIC |
Fair point on the spec language. Though in practice I had a 45 min session with no noticeable tracking drift or issues. Since the conversion happens fresh each frame (no stored offsets), clock drift can't really accumulate. Typical clock drift between sources is like 10-100 ppm (microseconds per second). Pose prediction only needs millisecond-level accuracy. Even if the clocks aren't perfectly synced, the per-frame error would be in microseconds - way below what would affect tracking.
Edit: Saw your second comment. So if QPC uses CLOCK_MONOTONIC_RAW/BOOTTIME and the timespec extension expects something else entirely, then even the passthrough approach wouldn't solve the mismatch. What would the correct implementation look like in that case? |
|
Using Quest 3S, with ALVR 20.13.0 and SteamVR beta 2.14.5 on NixOS 25.11, and this as proton for MSFS 2020 (build 19260129), my controllers froze up after about 15 minutes. I think I was turning my head around a lot when that happened, no idea if that correlates at all. Triggers kept working after. |
Can you post me the proton log so i can see what went wrong please? Currently from my testing this doesn't happen with Steam Link + Quest 3S on Garuda with my build |
Sure, I'll try to reproduce it tomorrow and paste logs. |
Thanks! |
I tried really hard to replicate your error but after an hour of flying my controllers are still working fine, maybe this was an issue with ALVR? |
|
An OpenXR runtime is free to use whatever it fits as the origin of time. You should use |
@xytovl Looked into using XR_KHR_convert_timespec_time but the thunk infrastructure to call those functions from the Wine side doesn't exist. clock_gettime/CLOCK_MONOTONIC aren't available in the Windows DLL, would need to add Unix-side thunks for both timespec conversion functions. Given the current implementation works in practice (tested 45+ min sessions and the actual fix itself does what it was supposed to), is that complexity worth it? Happy to add the thunks if needed but it's a significantly larger change. |
|
Edit: |
Yeah, QPC is explicitly not an absolute timestamp, it's relative to boot with no defined epoch. The spec just needs consistent round-trip conversion within a session, which this provides. Happy to add the full thunk infrastructure if maintainers want it, but given the complexity vs. practical benefit, seemed like overkill. Will see what they say. |
|
steam-1250410.log |
@imol-ai can you add to the launch commands |
log.tar.gz |
|
Sorry, @PhialsBasement, I was dumb, and I switched the game into VR mode, right on the loading screen. Now, I am switching it into VR mode only when it reaches the main menu, and now the controllers work again properly. I will reproduce the ~15 minute freeze later, with logs, please disregard my earlier dumb logs :) |
The implementation is not just fine, XrTime can be obtained through a different API that is simply forwarded to the application. If the QPC to XrTime conversion is wrong and the application uses both XrTimes for different things then you will run into issues |
The XrTime values from the runtime (xrLocateViews, xrWaitFrame, etc.) and the converted QPC times are in the same domain. The conversion maps QPC ticks to the runtime's XrTime epoch. If both came from different epochs the entire extension would be useless since apps need to correlate their own timestamps with runtime timestamps. |
Glad it wasn't anything with the build, let me know if you have encountered the 15min error or not. |
I don't understand what you mean by this so I will just give an example:
There is no guarantee that the epochs are the same. In fact they are encouraged to pick an epoch that is not the system clock epoch:
I gave it some thought and I think I found a slightly better implementation; here is some rough code I came up with |
You're right about the epoch assumption. In practice SteamVR and Monado both use boot-relative time so it works, but it's not guaranteed. Happy to implement the proper offset-based approach if maintainers want it, but that's a significantly larger change touching multiple files with Unix thunks that would require wayyyy larger sign off than this which is unlikely to change unless SteamVR or Monado commit to making a change that would break a hell of a lot more things than just this. |
I actually really like this idea, I was already speculating on how I'd do it, but this is way better, so if it does end up needing to be done ill take inspiration from this |
Yep, happened again multiple times. Log: |
i notice at some point, the func passes 0xffffff33b52018a4(negative 877.4sec) to the waitframe, i am assuming theres an overflow, i need to build a more overflow resistant formula. |
@imol-ai i've now made a more overflow resistant formula https://github.com/PhialsBasement/Proton/releases/tag/msfs-fix-oflowres |
fwiw I really doubt there's overflow XrTime would require 250+ years to overflow. QPC would be even longer |
You're right that XrTime itself won't overflow for 250+ years, but the overflow is in the intermediate calculation, not the stored value. The old formula was At 10 MHz frequency, when predictedDisplayTime reaches ~922 billion ns (~15.4 minutes) which when that happens: The multiplication overflows before the division happens, wrapping to negative. The fix splits the calculation to avoid the intermediate overflow: |
|
@PhialsBasement I've tested it extensively, it seems that your fix holds. Thank you for all of your work! |
That's not the old formula, I asked you to change it to |
My bad, I described the wrong formula. that was the original 0a32156 formula, not 72ae486. Interestingly enough, @imol-ai was hitting the 15-min freeze on 72ae486, and b9b2eaa fixed it. So 72ae486 still had an issue, whether it's the freq > 1e9 edge case causing 1e9/freq = 0, precision loss from integer division, or something else we haven't pinpointed yet. The split formula is more robust regardless, handles any frequency, preserves precision, and most importantly, it actually fixed the issue for everyone now. |
frequency is always less than NSECPERSEC fwiw, I would like to know why the old approach didn't work. It doesn't seem like multiplying by 100 is going to cause any overflow any time soon. |
Could be precision loss from the integer truncation in 1e9/freq, could be the other direction's formula, or something environment-specific with ALVR/NixOS. Didn't see anything as such in the logs to pinpoint it further. |
|
Hi! Thanks for the patches. I am a Proton developer (particularly dealing with vrclient / wineopenxr in Proton) and had a look at the patch. To do it right, I think this should be based on xrConvertTimespecTimeToTimeKHR present on Linux (instead of assuming that QPC timeline matches XrTime without any offset). You can look at Wine's dll/winevulkan/vulkan.c:convert_monotonic_timestamp() to see how we currently convert between that timespec (monotonic) time and QPC time. Then, currently there is a hack which makes XR_KHR_win32_convert_performance_counter_time / xrConvertWin32PerformanceCounterToTimeKHR always available regardless of whether XR_KHR_convert_timespec_time is available. I guess we might want to still keep the hack, but before calling xrConvertTimespecTimeToTimeKHR and reverse we'd need to check that the native function is available (and if not, fallback to current behaviour returning error). If updating the patches, could you please not fixup with new commits on top but instead edit the commits / squash the changes (while functionally different parts may go into different patches if possible, in a way that intermediate commits do not introduce non-working code). It is easier to look at that this way and we keep linear history without merge commits and only merge the final ones. Would you please let me know if you are planning to continue working on this soon to make it right? If not, I might come to do it myself at some moment, or otherwise wait and review your new version. |
Thanks for the feedback! Yes, I'll continue working on this. Will implement it properly using xrConvertTimespecTimeToTimeKHR and reference convert_monotonic_timestamp() for the timespec↔QPC conversion. Will squash into clean commits when ready. |
I just looked at convert_monotonic_timestamp, while it works for the case of calibrated timestamps (I failed to notice there's no offset with the clock_boottime path lol). it doesn't work for us (without modification), since openxr only uses the monotonic clock. Additionally, the QPC tick value we are getting in upstream wine is wrong right now, and I don't really know how to fix it (upstream now uses clock boottime, but winevulkan assumes monotonic raw is used) since boottime clock doesn't exist in VK_EXT_calibrated_timestamps. I'll open a draft PR to get some help on this, I think could be rather problematic if it doesn't get addressed. |
|
Yes, the difference between CLOCK_MONOTONIC, CLOCK_MONOTONIC_RAW and CLOCK_BOOTTIME is important but if the game works with the simple implementation, ignoring this difference, I'd suggest to start with such and leave a more complicated decision for later if it will be needed (maybe following the solution to be introduced for Vulkan clock adjustment). I'd only add that even this implementation should go not to loader.c but to Unix side openxr.c at once (first adding the functions being implemented to FUNCTION_OVERRIDES in make_openxr, similar to other overridden functions like, e. g., xrGetVulkanGraphicsDeviceKHR). |
0310155 to
f4e4c9b
Compare
|
@gofman Pushed the implementation to unix side with FUNCTION_OVERRIDES as requested. Uses xrConvertTimespecTimeToTimeKHR with fallback to simple conversion if native functions unavailable, built and tested it and it works as expected. Ready for review. |
f4e4c9b to
aaf1c70
Compare
|
Small correction: just fixed to actually use FUNCTION_OVERRIDES instead of MANUAL_LOADER_THUNKS. |
gofman
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh, I didn't finish reviewing it when the patch got updated. So I guess I will post these comments and then look once again in the updated version which will hopefully become shorter.
wineopenxr/openxr.c
Outdated
| ALL_XR_INSTANCE_FUNCS() | ||
| #undef USE_XR_FUNC | ||
|
|
||
| /* Load timespec conversion functions (not in dispatch table since XR_USE_TIMESPEC is Linux-only). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove this part of comment, while leaving the second line.
wineopenxr/openxr.c
Outdated
| const char *win32_ext, *linux_ext; | ||
| BOOL remove_original; | ||
| BOOL force_enable; | ||
| BOOL *available; /* Set to TRUE if linux extension is actually supported */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This added field is probably not needed?
wineopenxr/openxr.c
Outdated
| }; | ||
|
|
||
| /* Track whether XR_KHR_convert_timespec_time is available */ | ||
| static BOOL g_timespec_time_available = FALSE; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see this variable used (while it is assigned), that is probably a leftover superseded by function pointer check and should be removed?
wineopenxr/openxr.c
Outdated
| g_timespec_time_available = TRUE; | ||
| TRACE("XR_KHR_convert_timespec_time is available.\n"); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, this added block is probably not needed.
wineopenxr/openxr.c
Outdated
| secs = performanceCounter->QuadPart / TICKSPERSEC; | ||
| remainder = performanceCounter->QuadPart % TICKSPERSEC; | ||
| ts.tv_sec = secs; | ||
| ts.tv_nsec = (remainder * NANOSECONDS_IN_A_SECOND) / TICKSPERSEC; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably "remainder * (NANOSECONDS_IN_A_SECOND / TICKSPERSEC)" would be more obvious (avoiding a possibility for overflow, while I know this is a remainder and functionally would be probably fine either way). We can use the fact that NANOSECONDS_IN_A_SECOND is divisible into TICKSPERSEC, same as in winevulkan.
wineopenxr/openxr.c
Outdated
| /* Convert timespec to QPC ticks (Wine QPC is 10MHz = 100ns ticks). | ||
| * tv_nsec is always < 1e9, so tv_nsec * TICKSPERSEC won't overflow. */ | ||
| performanceCounter->QuadPart = ts.tv_sec * TICKSPERSEC + | ||
| (ts.tv_nsec * TICKSPERSEC) / NANOSECONDS_IN_A_SECOND; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, in this case it is probably clearer to divide constants before multiplying.
wineopenxr/openxr.c
Outdated
|
|
||
| /* Convert QPC ticks to timespec (Wine QPC is 10MHz = 100ns ticks). | ||
| * Split calculation to avoid overflow while maintaining precision. */ | ||
| secs = performanceCounter->QuadPart / TICKSPERSEC; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can remove extra unneeded variables 'secs, remainder' and assign directly to the structure fields.
Will clean these up. Let me know if there's anything else once you finish reviewing. |
… native timespec conversion Implement xrConvertWin32PerformanceCounterToTimeKHR and xrConvertTimeToWin32PerformanceCounterKHR by calling the native Linux xrConvertTimespecTimeToTimeKHR/xrConvertTimeToTimespecTimeKHR functions. This properly handles any offset between CLOCK_MONOTONIC and XrTime rather than assuming QPC directly maps to XrTime. Wine's QPC is based on CLOCK_MONOTONIC with 100ns (10MHz) ticks. The conversion uses overflow-safe arithmetic for long-running systems. Returns XR_ERROR_FUNCTION_UNSUPPORTED if the native timespec conversion extension is not available.
aaf1c70 to
110f98c
Compare
|
Looks good to me now, but have to wait for gofman's review ofc |
|
Thanks! I've pushed the patch to Proton Experimental with the following additional cleanups:
I wasn't checking if that works with MSFS. The commit is currently available in Proton Experimental bleeding-edge branch (https://github.com/ValveSoftware/Proton/wiki/Proton-Versions#proton-bleeding-edge), the source is here: https://github.com/ValveSoftware/Proton/commits/experimental-bleeding-edge-10.0-290998-20260107-peaca45-wa21b86f-df76b0e-vc60d88/. Now I think we can address the clock mismatch as a separate patch. I think we can call NtQueryPerformanceCounter( &counter, NULL), and then clock_gettime( CLOCK_MONOTONIC, &ts ) and convert it to QPC units with 'ts.tv_sec * (ULONGLONG)TICKSPERSEC + ts.tv_nsec / 100'. Then, add or subtract the difference in corresponding functions. That has to be done in each xrConvert... call as the difference may change any time and thus we can't precompute it. I think we can ignore an inaccuracy introduced by the two calls performed at a slightly different time, those calls are very quick and the inaccuracy should be neglectable in this case. Calling NtQueryPerformanceCounter instead of clock_gettime( ) will spare us from knowledge which Unix side timer is backing NtQueryPerformanceCounter (and that will change in Proton 11 indeed, and in theory may change once again). If you want to do that please ping me once you open a PR with that. |
Link: ValveSoftware#9347 CW-Bug-Id: #26405



Implements both xrConvertWin32PerformanceCounterToTimeKHR and xrConvertTimeToWin32PerformanceCounterKHR for converting between QueryPerformanceCounter ticks and XrTime nanoseconds.
Fixes VR controller tracking in Microsoft Flight Simulator 2020, where controllers would spawn at the HMD position due to failed time conversion. Debug logs showed these functions being called every frame for pose timing.