It's been said on the forums many times already: You need to be careful when measuring OpenGL so naively.
As said many times before, OpenGL is probably the most asynchronous API you will ever see. You need to be lucky (or just very knowledgeable) to run into a function that actually blocks CPU execution until all its effects are complete when writing your typical (not purposely stupid) OpenGL code. Timing specific functions and assuming that the corresponding OpenGL operation must be responsible for any delays is just wrong. Under the hood, nothing prevents the driver from doing something you previously requested only when calling another function in the future.
There are a few things that have to be stated:
- Because you activated V-Sync and presumably are using a 60Hz monitor, the driver will try to target a 16ms frame.
- Where/When the driver does the waiting is completely implementation defined. Typically it does the waiting in the buffer swap i.e. inside sf::Window::display(). However, there is nothing preventing it from waiting somewhere else if it has good reason to do so. Double/Triple buffering allows it to stretch time a little since queued frames are only sent out 1-2 frames later.
- On my system, the delay when loading new textures doesn't occur consistently when loading the first texture. It jumps about between the first 4 textures. However, the delay is always the same.
- If you time the .display() call and/or the whole frame you will notice that a) the .display() call will not block for the usual 16ms when one of the delays during texture loading occurs and b) the total frame time is 16ms, meaning that all the delay either comes from .display() or texture loading.
Now, we need to ask ourselves why we notice stuttering. Humans have a more or less good ability to perceive changes better than "absolute" values. This means in our case, we notice stuttering not because a frame is just "slow" in general but because it is "slower" than all the other frames. A jump from 16ms frame time to 33ms frame time might already be enough to cause this, even though funnily enough our brains would not consider something as "constantly stuttering" if it ran at a constant 30 FPS or 33ms frame. What is happening here however is a bit more severe than 33ms which means that in those "longer frames" there is suddenly time that is unaccounted for.
Just digging through the call chain of sf::Texture::loadFromFile I noticed that there are still a few places left where glFlush() is still being called. glFlush()/glFinish() and friends are all pretty meh. They belong to those functions that were conceived in a time where single core CPUs were still a thing and GPUs were still glorified co-processors. There is really no good reason any more why you would need to use glFinish() in this day and age, there are so many better performing alternatives to the scenarios that would make it necessary. glFlush() is really weird. If you ask me it is mainly there to overcome driver implementation quirks left over from the early days. It might still be necessary in some special cases, but in typical situations, it really isn't. Like I said, SFML still makes use of glFlush(), now only in sf::Texture and sf::Shader because I threw a bunch of glFlush() calls out from other places just a few months ago.
Commenting out the glFlush()es actually "fixed" the stuttering for me. From my experience, glFlush() is a mixed bag. Sometimes you don't notice it, other times it can be almost as bad performance-wise as a glFinish(). I guess it depends on the driver and whether it likes you/your application/your system. What many people don't know is that an implicit flush is performed when switching OpenGL contexts as well. Put another way, if you want good performance, don't switch contexts if you don't have to. I already improved this a little with my sf::RenderTexture optimization in SFML 2.5. What to do with the remaining explicit glFlush()es I haven't thought about yet. It will be hard to work around since sf::RenderTarget caches state really aggressively and you would need to rebind resources if you left out the glFlush()es.
If you feel brave and like to take risks, you can comment out the glFlush()es and see if it works out for you.