0

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 &copy);
        Texture &operator=(const Texture &copy);
    #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?

salbeira
  • 2,375
  • 5
  • 26
  • 40
  • yes, it makes sense. But why do you load the resource and them move them around? Since you wait to be done loading, couldn't you just load them in-place? – Jeffrey Apr 14 '21 at 17:49
  • I could but during the time this happens the screen would be unresponsive. I want to prevent that by displaying some interactive loading screen or anything else, but I desperatly want to avoid the good old days of a loading bar that slowly fills in undiscernable intervals and instead just want to make sure that *some* animation is resolving to visualize that stuff is actually happening. – salbeira Apr 14 '21 at 17:54
  • This part I get. I was confused since your snippet starts with `TextureData & operator=(TextureData other)` that this was actually needed. But it seems your load actually loads in the object. So, nvm – Jeffrey Apr 14 '21 at 17:57
  • "*keeping track of what resource type is even capable of being shared is a small headache in and of itself*" That all depends on what you mean by "resource type". The only things I would call "resources" are buffers or textures, both of which are shared. You don't load FBOs from a file, or query objects or whatever else. Even VAOs shouldn't be thought of as "resources". – Nicol Bolas Apr 14 '21 at 18:15
  • All these things need an ID obtained by glCreateX functions that serves as the access handle to these objects and as such I would consider them resources that should not be copied. I am not sure about "state" in VAOs, I always need to re-read the docs about how they are supposed to be used and how they actually function in the background but even VAOs have a GLuint handle-ID. As such a wrapper class representing them should also be responsible for their destruction / unloading. – salbeira Apr 14 '21 at 18:39

0 Answers0