Going through several peculiarities of OpenGL, multithreading with OpenGL is a rather difficult subject. Though shared contexts exist, there is no guarantee that an OpenGL resource is correctly initialized, uploaded and shared between contexts and keeping track of what resource type is even capable of being shared is a small headache in and of itself (yes, keeping a list from (in-)official docs is possible, there is still no single way to handle ALL resource types without resorting to the solution in question).
One recommendation I got was to deal with the loading of resources from disk in a separate thread while doing the upload to the GPU on the main thread. Perfect opportunity for a loading screen.
So there are several steps I want to do to optimize this process. I therefore split the data I want to upload to the GPU into the handle on the GPU (Texture class) and the data on the CPU (TextureData class).
class TextureData { // Data can be copied without a problem
GLuint width;
GLuint height;
GLvoid *data;
public:
friend void swap(TextureData &a, TextureData &b) noexcept {
using std::swap;
swap(a.width, b.width);
swap(a.height, b.height);
swap(a.data, b.data);
}
TextureData() : width(0), height(0), data(nullptr) {}
TextureData(std::string filename);
TextureData(GLuint width, GLuint height, GLvoid* data) : width(width), height(height), data(data) {}
// There is a conceptual problem with ownership of the data pointer about this constructor though
TextureData(TextureData &other) : width(other.width), height(other.height), data(other.data) {}
// Similarily here ...
TextureData(TextureData &&other) : TextureData()
{
using std::swap;
swap(*this, other);
}
TextureData & operator=(TextureData other)
{
using std::swap;
swap(*this, other);
return *this;
}
~TextureData() {
delete data; // I expect the pointer isn't needed anymore after uploading the data but if there is a better way to make ownership of the data pointer more clear, feel free to elaborate
}
GLuint get_width() {
return this->width;
}
GLuint get_height() {
return this->height;
}
GLvoid *get_data() {
return this->data;
}
};
With the actual Texture that is relevant for everything else being simply that:
class Texture { // Handles a resource handle (the numerical ID of the texture) and therefore is not copiable
GLuint texture;
GLuint width;
GLuint height;
public:
Texture();
Texture(TextureData &data); // In theroy, data can be freed right after construction
Texture(GLuint width, GLuint height, GLvoid* data); // Similar here
Texture(Texture &&temp); // Move Constructor
~Texture();
Texture & operator=(Texture &&temp); // Move Assignment
#ifdef ALLOW_TEXTURE_COPY
Texture(const Texture ©);
Texture &operator=(const Texture ©);
#endif
void bind();
GLuint get_name() const { return this->texture; }
GLuint get_width() const { return this->width; }
GLuint get_height() const { return this->height; }
void read_size(GLuint &width, GLuint &height) const;
};
So if I want to draw an image to the scene for example:
class Scene {
struct TextureInfo {
TextureData *data;
Texture &resource;
};
Texture image;
std::mutex load_mutex;
std::vector<TextureInfo> loaded;
public:
Scene(ThreadPool pool) {
pool.task([this](){
TextureInfo info;
info.data = load_image("res/image.png");
info.resource = this->image;
std::unique_lock<std::mutex> lock(this->load_mutex);
this->loaded.emplace_back(info);
});
}
void draw(Renderer r) {
if(!all_resources_loaded) { // however I find that out
draw_loading_screen(); // spinning wheels wheeeeeee
} else {
r.draw_image(0, 0, image);
}
}
void update(double time) { // gets called in the main thread the OpenGL context is made current in ; also: double time *drums the beat*
{
std::unique_lock<std::mutex> lock(this->load_mutex);
for(TextureInfo info : this->loaded) {
info.resource = Texture(*info.data);
//glFinish();
/* I remember a case where the upload itself is actually off-thread and
* therefore the deletion of the data in the next line can corrupt the data
* that openGL fetches in the meantime, but I have no idea if that was
* because of the shared context I used before or because of some other
* reason */
delete info.data;
}
this->loaded.clear();
}
if(!all_resources_loaded) {
update_loading_screen();
} else {
update_scene();
}
}
So my question is basically ... is this sane? Are there alternatives? Are the assumtions about what is and is not a resource correct? Would there be strategies that are a major improvements?