SUMMARY
It seems that vsync with OpenGL is broken on Windows in windowed mode. I've tried different APIs (SDL, glfw, SFML), all with the same result: While the framerate is limited (and consistently around 16-17 ms according to CPU measurements on multiple 60 Hz setups I've tried), and the CPU is in fact sleeping most of the time, frames are very often skipped. Depending on the machine and the CPU usage for things other than rendering, this can be as bad as effectively cutting the frame rate in half. This problem does not seem to be driver related.
How to have working vsync on Windows with OpenGL in windowed mode, or a similar effect with these properties (if I forgot something notable, or if something is not sensible, please comment):
- CPU can sleep most of the time
- No tearing
- No skipped frames (under the assumption that the system is not overloaded)
- CPU gets to know when a frame has actually been displayed
DETAILS / SOME RESEARCH
When I googled opengl vsync stutter or opengl vsync frame drop or similar queries, I found that many people are having this issue (or a very similar one), yet there seems to be no coherent solution to the actual problem (many inadequately answered questions on the gamedev stackexchange, too; also many low-effort forums posts).
To summarize my research: It seems that the compositing window manager (DWM) used in newer versions of Windows forces triple buffering, and that interferes with vsync. People suggest disabling DWM, not using vsync, or going fullscreen, all of which are not a solution to the original problem (FOOTNOTE1). I have also not found a detailed explanation why triple buffering causes this issue with vsync, or why it is technologically not possible to solve the problem.
However: I've also tested that this does not occur on Linux, even on VERY weak PCs. Therefore it must be technically possible (at least in general) for OpenGL-based hardware acceleration to have functional vsync enabled without skipping frames.
Also, this is not a problem when using D3D instead of OpenGL on Windows (with vsync enabled). Therefore it must be technically possible to have working vsync on Windows (I have tried new, old, and very old drivers and different (old and new) hardware, although all the hardware setups I have available are Intel + NVidia, so I don't know what happens with AMD/ATI).
And lastly, there surely must be software for Windows, be it games, multimedia applications, creative production, 3D modeling/rendering programs or whatever, that use OpenGL and work properly in windowed mode while still rendering accurately, without busy-waiting on the CPU, and without frame drops.
I've noticed that, when having a traditional rendering loop like
while (true)
{
poll_all_events_in_event_queue();
process_things();
render();
}
The amount of work the CPU has to do in that loop affects the behavior of the stuttering. However, this is most definitely not an issue of the CPU being overloaded, as the problem also occurs in one of the most simple programs one could write (see below), and on a very powerful system that does nothing else (the program being nothing other than clearing the window with a different color on each frame, and then displaying it).
I've also noticed that it never seems to get worse than skipping every other frame (i.e., in my tests, the visible framerate was always somewhere between 30 and 60 on a 60 Hz system). You can observe somewhat of a Nyquist sampling theorem violation when running program that changes the background color between 2 colors on odd and even frames, which makes me believe that something is not synchronized properly (i.e. a software bug in Windows or its OpenGL implementation). Again, the framerate as far as the CPU is concerned is rock solid. Also, timeBeginPeriod has had no noticeable effect in my tests.
(FOOTNOTE1) It should be noted though that, because of the DWM, tearing does not occur in windowed mode (which is one of the two main reasons to use vsync, the other reason being making the CPU sleep for the maximum amount of time possible without missing a frame). So it would be acceptable for me to have a solution that implements vsync in the application layer.
However, the only way I see that being possible is there is a way to explicitly (and accurately) wait for a page flip to occur (with possibility of timeout or cancellation), or to query a non-sticky flag that is set when the page is flipped (in a way that doesn't force flushing the entire asynchronous render pipeline, like for example glGetError does), and I have not found a way to do either.
Here is some code to get a quick example running that demonstrates this problem (using SFML, which I found to be the least painful to get to work).
You should see homogenous flashing. If you ever see the same color (black or purple) for more than one frame, it's bad.
(This flashes the screen with the display's refresh rate, so maybe epilepsy warning):
// g++ TEST_TEST_TEST.cpp -lsfml-system -lsfml-window -lsfml-graphics -lGL
#include <SFML/System.hpp>
#include <SFML/Window.hpp>
#include <SFML/Graphics.hpp>
#include <SFML/OpenGL.hpp>
#include <iostream>
int main()
{
// create the window
sf::RenderWindow window(sf::VideoMode(800, 600), "OpenGL");
window.setVerticalSyncEnabled(true);
// activate the window
window.setActive(true);
int frame_counter = 0;
sf::RectangleShape rect;
rect.setSize(sf::Vector2f(10, 10));
sf::Clock clock;
while (true)
{
// handle events
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
{
return 0;
}
}
++frame_counter;
if (frame_counter & 1)
{
glClearColor(0, 0, 0, 1);
}
else
{
glClearColor(60.0/255.0, 50.0/255.0, 75.0/255.0, 1);
}
// clear the buffers
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Enable this to display a column of rectangles on each frame
// All colors (and positions) should pop up the same amount
// This shows that apparently, 1 frame is skipped at most
#if 0
int fc_mod = frame_counter % 8;
int color_mod = fc_mod % 4;
for (int i = 0; i < 30; ++i)
{
rect.setPosition(fc_mod * 20 + 10, i * 20 + 10);
rect.setFillColor(
sf::Color(
(color_mod == 0 || color_mod == 3) ? 255 : 0,
(color_mod == 0 || color_mod == 2) ? 255 : 0,
(color_mod == 1) ? 155 : 0,
255
)
);
window.draw(rect);
}
#endif
int elapsed_ms = clock.restart().asMilliseconds();
// NOTE: These numbers are only valid for 60 Hz displays
if (elapsed_ms > 17 || elapsed_ms < 15)
{
// Ideally you should NEVER see this message, but it does tend to stutter a bit for a second or so upon program startup - doesn't matter as long as it stops eventually
std::cout << elapsed_ms << std::endl;
}
// end the current frame (internally swaps the front and back buffers)
window.display();
}
return 0;
}
System info:
Verified this problem on these systems:
- Windows 10 x64 i7-4790K + GeForce 970 (verified that problem does not occur on Linux here) (single 60 Hz monitor)
- Windows 7 x64 i5-2320 + GeForce 560 (single 60 Hz monitor)
- Windows 10 x64 Intel Core2 Duo T6400 + GeForce 9600M GT (verified that problem does not occur on Linux here) (single 60 Hz laptop display)
- And 2 other people using Windows 10 x64 and 7 x64 respectively, both "beefy gaming rigs", can request specs if necessary
UPDATE 20170815
Some additional testing I've done:
I tried adding explicit sleeps (via the SFML library, which basically just calls Sleep from the Windows API while ensuring that timeBeginPeriod is minimal).
With my 60 Hz setup, A frame should ideally be 16 2/3 Hz. According to QueryPerformanceCounter measurements, my system is, most of the time, very accurate with those sleeps.
Adding a sleep of 17 ms causes me to render slower than the refresh rate. When I do this, some frames are displayed twice (this is expected), but NO frames are dropped, ever. The same is true for even longer sleeps.
Adding a sleep of 16 ms sometimes causes a frame to be displayed twice, and sometimes causes a frame to be dropped. This is plausible in my opinion, considering a more or less random combination of the result at 17 ms, and the result at no sleep at all.
Adding a sleep of 15 ms behaves very similarly to having no sleep at all. It's fine for a short moment, then about every 2nd frame is dropped. The same is true for all values from 1 ms to 15 ms.
This reinforced my theory that the problem might be nothing other than some plain old concurrency bug in the vsync logic in the OpenGL implementation or the operating system.
I also did more tests on Linux. I hadn't really looked much into it before - I merely verified that the frame drop problem didn't exist there and that the CPU was, in fact, sleeping most of the time. I realised that, depending on several factors, I can make tearing consistenly occur on my test machine, despite vsync. As of yet, I do not know whether this that issue is connected to the original problem, or if it is something entirely different.
It seems like the better approach would be some gnarly workarounds and hacks, and ditching vsync altogether and implementing everything in the application (because apparently in 2017 we can't get the most basic frame rendering right with OpenGL).
UPDATE 20170816
I have tried to "reverse-engineer" a bunch of open source 3D engines (got hung up on obbg (https://github.com/nothings/obbg) in particular).
First, I checked that the problem does not occur there. The frame rate is butter smooth. Then, I added my good old flashing purple/black with the colored rects and saw that the stuttering was indeed minimal.
I started ripping out the guts of the program until I ended up with a simple program like mine. I found that there is some code in obbg's rendering loop that, when removed, causes heavy stutter (namely, rendering the main part of the obbg ingame world). Also, there is some code in the initialization that also causes stutter when removed (namely, enabling multisampling). After a few hours of fiddling around it seems that OpenGL needs a certain amount of workload to function properly, but I have yet to find out what exactly needs to be done. Maybe rendering a million random triangles or something will do.
I also reaslised that all my existing tests behave slightly differently today. It seems that I have overall fewer, but more randomly distributed frame drops today than the days before.
I also created a better demo project that uses OpenGL more directly, and since obbg used SDL, I also switched to that (although I briefly looked over the library implementations and it would surprise me if there was a difference, but then again this entire ordeal is a surprise anyway). I wanted to approach the "working" state from both the obbg-based side, and the blank project side so I can be really sure what the problem is. I just put all the required SDL binaries inside the project; if as long as you have Visual Studio 2017 there should be no additional dependencies and it should build right away. There are many #ifs that control what is being tested.
https://github.com/bplu4t2f/sdl_test
During the creation of that thing I also took another look how SDL's D3D implementation behaves. I had tested this previously, but perhaps not quite extensively enough. There were still no duplicate frames and no frame drops at all, which is good, but in this test program I implemented a more accurate clock.
To my surprise I realised that, when using D3D instead of OpenGL, many (but not the majority) loop iterations take somewhere between 17.0 and 17.2 ms (I would not have caught that with my previous test programs). This does not happen with OpenGL. The OpenGL rendering loop is consistently in the range 15.0 .. 17.0. If it is true that sometimes there needs to be a slightly longer waiting period for the vertical blank (for whatever reason), then OpenGL seems to miss that. That might be the root cause of the entire thing?
Yet another day of literally staring at a flickering computer screen. I have to say I really did not expect to spend that amount of time on rendering nothing but a flickering background and I'm not particularly fond of that.