Параллельная загрузка ресурсов для OpenGL

#c #multithreading #opengl #raii

Вопрос:

Учитывая несколько особенностей OpenGL, многопоточность с помощью OpenGL-довольно сложная тема. Хотя общие контексты существуют, нет никакой гарантии, что ресурс OpenGL правильно инициализирован, загружен и разделен между контекстами, и отслеживание того, какой тип ресурса может быть общим, само по себе является небольшой головной болью (да, возможно ведение списка из (в)официальных документов, по-прежнему нет единого способа обработки ВСЕХ типов ресурсов, не прибегая к рассматриваемому решению).

Одна из рекомендаций, которую я получил, заключалась в том, чтобы заниматься загрузкой ресурсов с диска в отдельном потоке при загрузке на графический процессор в основном потоке. Прекрасная возможность для экрана загрузки.

Поэтому я хочу сделать несколько шагов, чтобы оптимизировать этот процесс. Поэтому я разделяю данные, которые хочу загрузить на графический процессор, на дескриптор графического процессора (класс текстуры) и данные на процессоре (класс TextureData).

 class TextureData { // Data can be copied without a problem
        GLuint width;
        GLuint height;
        GLvoid *data;
    public:

        friend void swap(TextureData amp;a, TextureData amp;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 amp;other) : width(other.width), height(other.height), data(other.data) {}
// Similarily here ...
        TextureData(TextureData amp;amp;other) : TextureData()
        {
            using std::swap;
            swap(*this, other);
        }

        TextureData amp; 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;
        }
    };
 

С фактической текстурой, которая имеет отношение ко всему остальному, просто так:

 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 amp;data); // In theroy, data can be freed right after construction
        Texture(GLuint width, GLuint height, GLvoid* data); // Similar here
        Texture(Texture amp;amp;temp); // Move Constructor
        ~Texture();

        Texture amp; operator=(Texture amp;amp;temp); // Move Assignment

    #ifdef ALLOW_TEXTURE_COPY
        Texture(const Texture amp;copy);
        Texture amp;operator=(const Texture amp;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 amp;width, GLuint amp;height) const;
    };
 

Итак, если я хочу нарисовать изображение на сцене, например:

 class Scene {
    struct TextureInfo {
        TextureData *data;
        Texture amp;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();
        }
    }
 

Так что мой вопрос в основном таков … это нормально? Есть ли альтернативы? Верны ли предположения о том, что является и чем не является ресурсом? Будут ли какие-то стратегии, которые приведут к серьезным улучшениям?

Комментарии:

1. да, в этом есть смысл. Но почему вы загружаете ресурс, а они перемещают его? Поскольку вы ждете завершения загрузки, не могли бы вы просто загрузить их на место?

2. Я мог бы, но в то время, когда это произойдет, экран будет не реагировать. Я хочу предотвратить это, отобразив какой-нибудь интерактивный загрузочный экран или что-нибудь еще, но я отчаянно хочу избежать старых добрых дней панели загрузки, которая медленно заполняет неразличимые интервалы, и вместо этого просто хочу убедиться, что какая -то анимация разрешена, чтобы визуализировать, что на самом деле происходит.

3. Эту часть я понимаю. Я был сбит с толку, так как ваш фрагмент начинается с TextureData amp; operator=(TextureData other) того, что это действительно было необходимо. Но, похоже, ваш груз действительно загружается в объект. Итак, nvm

4. » отслеживание того, какой тип ресурсов вообще может быть общим, само по себе является небольшой головной болью». Все зависит от того, что вы подразумеваете под «типом ресурса». Единственные вещи, которые я бы назвал «ресурсами», — это буферы или текстуры, которые являются общими. Вы не загружаете файлы FBO из файла, не запрашиваете объекты или что-то еще. Даже VAOs не следует рассматривать как «ресурсы».

5. Все эти вещи нуждаются в идентификаторе, полученном функциями glCreateX, который служит дескриптором доступа к этим объектам, и поэтому я бы рассматривал их как ресурсы, которые не следует копировать. Я не уверен в «состоянии» в VAOs, мне всегда нужно перечитывать документы о том, как они должны использоваться и как они на самом деле функционируют в фоновом режиме, но даже у VAOs есть идентификатор дескриптора GLuint. Как таковой класс-оболочка, представляющий их, также должен отвечать за их уничтожение / выгрузку.