Hello everyone!
I want to address a problem that I have encountered with sf::VertexBuffer, which I am not able to figure out myself. Maybe one of you has a clue of what is going on. Sorry for the long text, but I want to describe the issue as precise as possible.
ConditionsThis is a problem which seems to only occur in a multi-thread setup. In the main thread, a sf::VertexBuffer is drawn to a window inside a render loop. Meanwhile, a second thread manipulates vertices in a vertex array (sf::Vertex*) and updates corresponding memory of the vertex buffer using its update() method. Additionally, in the main thread render loop, the sf::VertexBuffer::update() method is also called, updating the same vertices which the second thread is about to manipulate and update next.
BehaviorSometimes, the updating of vertices to the sf::VertexBuffer fails in the second thread, if same vertices were updated directly before by the main thread. For example, calling sf::VertexArray::update(
array +
i, 4,
i), which should update four vertices from
array at position
i in the vertex buffer, usually works fine in the second thread. But if the main thread also calls sf::VertexArray::update(
array +
i, 4,
i) shortly before, the second threads update can fail silently: update() still returns
true and no OpenGL error is raised, however no update seems to have actually occured.
DemonstrationBelow, I have put together an exemplary code which reproduces the issue.
After sf::RenderWindow and sf::VertexBuffer are created, a grid sized
blockResolution x
blockResolution of quad primitives is initialized using a sf::Vertex array. A second thread is then launched, which loops through all primitives, changing their color from blue to red, and back to blue after reaching the end, then to red again and so on. Simultaneously, in the main thread render loop, the method Interfere() performs an update to the buffer at position
nextVertex, which is also the position at which the second thread will update next. In order to exclude race conditions and to ensure the correct sequence of update() calls, all manipulations on both vertex array and buffer are protected by a std::mutex (although I understand that sf::VertexBuffer::update() is already thread safe by some kind of lock). When running the code, you can update the whole buffer by pressing G. By pressing H, you can toggle the calling of Interfere() in order to observe the difference.
Without Interfere(), the visual output will look something like this:
With Interfere(), you can clearly see the primitives where update() did not work, as the color is not updated. (It's getting worse with higher frame rate because of faster render loop)
Why to use this setupIn case you find the described conditions stupid and far-fetched: They probably are, but I want to achieve a specific functionality. The second thread will be loading geometry from file and put primitives into the lowest free position of the vertex array, one by one, and updating the corresponding memory of the buffer. However, I want to be able to also add and remove primitives to the same vertex array from the main thread. When removing a primitive in the main thread, sf::VertexBuffer::update() has to be called, which works, but this makes the then freed space in the vertex array the lowest free position. Hence, the next adding of a primitive by the second thread can fail, because update() was called shortly before by the main thread. Of course, there are several obvious workarounds or different designs which could achieve the same functionality, but I feel this should work nevertheless.
Do you have any idea what the issue could be? Is it some limitation of the underlying gl functions, or am I just missing something crucial here? I've not read about anyone having a similar problem, therefore I might just be overlooking a stupid error in the code...
Thanks in advance!
EDIT: So, after some more reading, I understand a little better what is happening. Apparently, CPU synchronisation does not in any way guarantee GPU synchronisation (didn't know that). When adding
glFlush();
or
glFinish();
after the Interfere() call, no more problems seem to occur. But is this a legitimate workaround, or does it potentially add large overhead? Sorry, but my knowledge of OpenGL is too shallow to judge
#include "SFML/Graphics.hpp"
#include <mutex>
/* Settings */
const int blockResolution = 32;
const int windowResolution = 1024;
const int maxFramerate = 120;
/* Derived constants */
const float blockSize = (float)windowResolution / blockResolution;
const int blockCount = blockResolution * blockResolution;
const int vertexCount = blockCount * 4;
/* Custom vertex colors */
sf::Color colorOn = sf::Color(220, 85, 50);
sf::Color colorOff = sf::Color(60, 120, 200);
/* Global variables, accessible by both threads */
sf::VertexBuffer buffer;
std::mutex bufferMutex;
sf::Vertex* vertices = new sf::Vertex[vertexCount];
int nextVertex = 0;
void InitQuads()
{
int vertexIndex = 0;
for (int x = 0; x < blockResolution; x++)
{
for (int y = 0; y < blockResolution; y++)
{
sf::FloatRect blockRect;
blockRect.left = x*blockSize;
blockRect.top = y*blockSize;
blockRect.width = blockSize;
blockRect.height = blockSize;
vertices[vertexIndex+0].position =
sf::Vector2f(blockRect.left, blockRect.top);
vertices[vertexIndex+1].position =
sf::Vector2f(blockRect.left, blockRect.top + blockRect.height);
vertices[vertexIndex+2].position =
sf::Vector2f(blockRect.left + blockRect.width, blockRect.top + blockRect.height);
vertices[vertexIndex+3].position =
sf::Vector2f(blockRect.left + blockRect.width, blockRect.top);
vertices[vertexIndex+0].color = colorOff;
vertices[vertexIndex+1].color = colorOff;
vertices[vertexIndex+2].color = colorOff;
vertices[vertexIndex+3].color = colorOff;
vertexIndex += 4;
}
}
buffer.update(vertices);
}
void ThreadLoop()
{
bool switchOn = true;
while (true)
{
bufferMutex.lock();
sf::Color setColor = switchOn ? colorOn : colorOff;
vertices[nextVertex+0].color = setColor;
vertices[nextVertex+1].color = setColor;
vertices[nextVertex+2].color = setColor;
vertices[nextVertex+3].color = setColor;
bool success = buffer.update(vertices + nextVertex, 4, nextVertex);
if (!success)
std::cout << "Buffer update has failed" << std::endl;
nextVertex += 4;
if (nextVertex == vertexCount) // Start from the beginning and toggle target vertex color
{
nextVertex = 0;
switchOn = !switchOn;
}
bufferMutex.unlock();
sf::sleep(sf::milliseconds(10)); // Pause to create some time between vertex buffer updates
}
}
void Interfere()
{
bufferMutex.lock();
bool success = buffer.update(vertices + nextVertex, 4, nextVertex);
if (!success)
std::cout << "Buffer update has failed (main thread)" << std::endl;
bufferMutex.unlock();
}
int main()
{
sf::RenderWindow window(
sf::VideoMode(windowResolution, windowResolution), "");
window.setFramerateLimit(maxFramerate);
buffer.create(vertexCount);
buffer.setPrimitiveType(sf::Quads);
buffer.setUsage(sf::VertexBuffer::Dynamic);
InitQuads(); // Create grid of blockResolution^2 quad primitives
sf::Thread th(ThreadLoop);
th.launch();
bool callInterfere = true;
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
window.close();
if (event.type == sf::Event::KeyPressed)
{
if (event.key.code == sf::Keyboard::G) // Key G updates the whole buffer
{
buffer.update(vertices);
std::cout << "Buffer updated" << std::endl;
}
if (event.key.code == sf::Keyboard::H) // Key H toggles main thread buffer updates
{
callInterfere = !callInterfere;
std::cout << "Main thread buffer updates " << callInterfere << std::endl;
}
}
}
if (callInterfere)
Interfere();
/* Draw all primitives to window */
window.clear();
bufferMutex.lock();
window.draw(buffer);
bufferMutex.unlock();
window.display();
}
th.terminate();
delete[] vertices;
return 0;
}
(Specs: i7-8700, GTX 1080 with latest driver version 461.09, Asus Prime Z370-P)