sway: FreeSync stops working in frame capped games when moving mouse
sway version 1.7
Config is default with exception of output $monitor mode 1920x1080@164.995Hz adaptive_sync on
Behavior:
- I start a game (for example Overwatch, or Witcher 3 - both are running through wine). Normally the game would run at a framerate higher than 165fps
- I cap the framerate (for example with Overwatchs ingame limiter, MangoHud, or Gamescope), to something lower, for example to 100fps
- FreeSync is working, which I can see because my monitor displays the current refresh rate
- As soon as I move the mouse, the refresh rate jumps up to 165
.
- Changing fullscreen or windowed mode does not make a difference
- Using Gamescope does not make a difference
- Keyboard input does not have this effect
- The effect does not always occur when the game is GPU limited (for example if it runs at 100fps without being frame capped). I tested a bit more, and I was able to see the effect when being GPU limited, but it’s not as clear, and more erratic. I assume this is because if the game is GPU limited, the GPU is not capable of rendering the extra frames.
- The effect does not occur in Plasma Wayland (with Kwin)
From what I understand, this behavior would be desired if a hardware cursor was displayed. However, it also occurs when there is no hardware cursor.
This is a problem, because limiting the frame rate in order to make use of FreeSync is desirable to get low input lag. The current behavior makes it impossible to use it.
About this issue
- Original URL
- State: open
- Created 2 years ago
- Reactions: 14
- Comments: 36 (20 by maintainers)
It sounds like you think
max_render_timedoes something different than what it does? It doesn’t configure the time to wait between renders. It configures how late before a monitor refresh to defer rendering when a frame is needed. If you set it to 1 ms sway will try to render the next frame 1 ms before the monitor refreshes, if there’s anything to render.And sure you could argue that the cursor moving is “something to render”. But if you let the cursor trigger that frame, it will lead to stutter. That’s literally what this issue you’re commenting on is about: Making the cursor not trigger frames. If the cursor moves and it causes sway to render/present its frame, if the game then finishes rendering its frame shortly after that, that game frame can’t be shown until the next smallest monitor refresh interval.
This is what one would want, where every time a game finishes rendering, sway can immediately render it and the monitor can immediately present it. Silky smooth gameplay.
Now say I move the cursor. And I let sway trigger a frame for that. Since this example monitor supports up to 100 Hz, with
max_render_timeset to1, sway would try to render it after 9 ms from the previous frame so it can have the frame ready to go at 10 ms. Since that’s the earliest the monitor can refresh. But if it does, you get stutter.Note the second 17 ms frame now being presented for 20 ms, and the third 17 ms frame being presented for 13 ms (numbers are rounded). The goal is to instead have it do this, while playing a game. Back to silky smooth gameplay.
So not having the cursor trigger a render is the entire point of my patch, as well as the point of this whole discussion. Obviously once the frame rate drops below something acceptable for interactivity it needs to start triggering frames for cursors again and you have to deal with the stutter. But for that, the time to wait with rendering would be better to base on the lowest refresh rate supported rather than the highest. On this example monitor it would then start letting the cursor trigger frames at 40 Hz instead. Or better yet, let the user configure the limit if they want.
The damage is mainly irrelevant. For one, only software cursors cause damage and most people very likely aren’t using software cursors. And secondly, as I already described above, the entire point of this issue/patch is to make cursor moves not trigger frames. The damage caused by software cursors just have to be reapplied before actually rendering.
I was trying to make sway not trigger new frames when moving the mouse a couple of months ago. Turns out it’s not too difficult to achieve if your needs aren’t very high. That said, I imagine most people have more needs than I do, but I haven’t managed to find the time to make it more generally usable. So instead I’m posting the patches here in case someone else wants to finish it and clean up the API a bit (and probably make it an option).
Specifically: With the patches applied, mouse input will never trigger a redraw if sway has a view fullscreened, and adaptive sync is enabled. If adaptive sync is not enabled, or nothing is fullscreened, mouse input triggers redraws as usual. Where that behavior falls a bit short of being “generally usable” is that it probably makes sense to limit it to some minimum refresh rate. As is, if you were to fullscreen a fully static window, you would never see your mouse cursor move. For my use that just happens to not be a problem since I only enable adaptive sync when I launch a game, and disable it again after (using scripts).
I’ve been using these patches for the past two months without issues. I also posted an earlier version of them in an AMD driver bug report before, and people replied to say it’s working for them too. (Though please don’t comment about the patches in that report, it just adds noise.)
Two things worth noting:
wlr_output_lock_software_cursors. I only added those to work around that AMD driver bug. They’re not necessary, and may not even be desirable as they prevent direct scanout. For my particular use case, preventing direct scanout was just the lesser of two evils.handle_damageinoutput.cassumes that the function only gets called for cursors. As far as I could tell, that was true at the time – and may still be true – but it’s probably a bad assumption to make.Github won’t let me attach the patch files for some reason, so I posted them to a gist instead: https://gist.github.com/rhoot/dcead8b195054387c067919fef499c5c
@rhoot I was just saying that because the implementation started out wrong, you should have approached it from a different angle (as per my original comment saying you shouldn’t turn it on and off in timer_handler).
I’ll leave it be now that you inspired me to write my own patch that does it the correct way™
@EVERYONE: VRR fix
This patch is for v1.9, so feel free to test it.
This supports any focused window! and allows you to set a max latency like so:
The value is in microseconds, this will force Factorio (a 60fps locked game) to run at 120fps when moving the cursor.
Right now there no way to read the EDID info for lowest support Hz so I’ve just hard coded 30fps as the limit for now.
https://github.com/YellowOnion/sway/commit/deferred-cursor-move
I do not see the AMD driver bug issue, but maybe I’m blind or just lucky with my card of choice. But I do see an issue with Scan out driving monitor at max fps, so might need to run with
-DnoscanoutI’ve opened up a MR on wlroots, https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4340
If I understand what you linked correctly, displaying hardware cursors with VRR is still being worked on and the behaviour is not ideal, as it currently always prioritises the cursor. This is an issue, but that should only happen when the hardware cursor is visible right, or am I misunderstanding something?
From this issue:
Doesn’t that mean that this is a seperate issue or a bug with sway? I also found this comment, which seems to be in line with what I think.
@GrabbenD I think you need to write a script to do that.
swaymsg -m -t SUBSCRIBE '["window"]It’s not dependant on your display, you can pick any number you want if you want the cursor to update at a specific rate:
1000000/Hz
Looks like you rebased the latest version cool 😃, neither wayland, wlroots, or sway have access to the edid VVR info (like minimum, or horizontal khz), which could help with better timing, I noticed some of the spikes myself (worst being 48hz app, driving monitor at 95hz lol), I did try to improve the accuracy after noticing a bug/oversight, but then replaced it with some other code that might make it worse off.
But yeah the current code is designed to deliver cursor updates in a timely manner, not a smooth manner.
For the sway user control, I was just trying to get some minimal implementation working, tbh I don’t like integer fps differences and how they lose precision the lower the fps is, lots of monitors are not actually 60fps (it depends on horizontal frequency, resolution, and the vblank time), especially with VRR on (I still need to workout the math for my monitor to confirm this), and all feature film is not 24fps (they’re officially defined as 24000/1001), TV also has a similar denominator, using time/latency has better precision for low fps scenarios, so I’m thinking about how to get better accuracy before just doing a the naive
_fpsparameter.I have a few ideas that could be combined.
_fpsparameter obviously should be included for a quick and easy option._denominatorwith a default parameter to 1 that can be changed for more accuracy._fpsinternally to the nearest integer division of the monitor, or something timed accurately with the edid VVR info.It’s also worth mention that I’m pretty sure Sway and wlroots don’t make any attempts to smooth updates in general either, from the code I read, which might explain why there’s reports of flicking in sway, if you have a windowed app open, and swaybar or a chat client updates for example, both force the display to update. This patch only changes the behavior of cursor redraws so it’s a little outside the scope of cursor management.
max_render_timealso has a heavy impact on timing, it’s currently using the max hz of display, and a low-res wayland timer, I was surprised that I could pull an extra 20% FPS setting it to 6ms, instead of “off”, which has me curious about how the entire graphics pipe interacts here.I updated your commits to work on latest
masterof sway/wlroots: https://gist.github.com/rhoot/5d47b0aabe96f6d44659d2f40329860fSeems to be working pretty well, once configured for a view. I tried running
gamescope -r 60 vkcubewith this and while my monitor did jump up to 65 Hz once or twice, that could also have been due to other factors. It only seemed to happen shortly after fullscreening it and then go back down to 60 and stay there. I’ll try running this for a while. 👍That said, I think it’d be nicer to make the config be specified in terms of updates per second instead of microseconds per frame. So something like
[title="Factorio"] min_cursor_fps 120. I thinkmax_cursor_latencymakes sense in the API, but in the user facing config thinking in terms of FPS is just way simpler.Don’t get me wrong: I never said there’s not a solution. I merely said I haven’t added one in my patches. The fact that it’s not “generally usable” is why it’s not a PR. I’m still waiting for the AMD bug on this to be fixed before I may have the motivation to try to do it properly if no one does it earlier.
Game frame time (more important than FPS for presenting) fluctuates. Even if you’re going at a rock solid 25 FPS, it will fluctuate by some 10-100 µs in one direction or the other. Add frame spikes and varying GPU loads between frames and you have a range of frame times that the game renders in as opposed to just one.
In order to apply your suggestion, sway would have to predict how long it will be from the previous frame before the game renders the next. If it mispredicts and the next frame is ready way sooner than sway had expected, it could cause the game frame to be delayed as sway had just triggered a refresh to update the cursor position, and refreshes can’t happen immediately after they have already happened. And the result would be that the game stutters for a frame, since the game had already assumed the time between the previous frame and this new frame.
Personally I would much rather have a dumb but reliable solution than one that tries to be smart. It will only have an effect when the game runs at a really low frame rate anyway, which (hopefully) is not typical. Basically just using the minimum rate supported by the monitor, or some user configurable value with some sensible default if that’s too low (e.g my minimum is 1 Hz). KWin’s PR on this appears to be leaning in the same direction, with using the maximum of 40 or the monitor’s lowest VRR rate.
Edit: I guess I should at least acknowledge games that intentionally limit rendering to something low, like 30 FPS. But those games typically don’t use a mouse since it would be a horrible experience, so cursor updates don’t apply to those games.
Did you look at the code in the patch? It doesn’t affect the timer at all. All it does is:
It doesn’t limit the redraw rate at all.
In case anyone is actually using my patches, I’ve updated the ones in the gist. Changes from before:
WLR_DRM_NO_ATOMIC=1). Previously doing so caused it to trigger redraws again when moving the mouse, but it seems that’s no longer the case.