Search code examples
c++opengl-esegl

OpenGL async texture loading


I'm using OpenGL ES 2 and am trying to load textures in the background using st::async however because I'm loading the image into a GL texture in a thread it doesnt work how its supposed to, I read on google that OpenGL isnt thread-safe to I think that's my issue, I mostly come here for advise on how I should implement my code so that most of the texture loading happens in the background without this issue, below is my current code

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

/// png format
GLuint load_texture(
    const GLsizei width, const GLsizei height,
    const GLenum  type,  const GLvoid *pixels)
{
    // create new OpenGL texture
    GLuint texture_object_id;
    glGenTextures(1, &texture_object_id);
    glBindTexture(GL_TEXTURE_2D, texture_object_id);
    // set texture filtering
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // generate texture from bitmap data

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
    // don't create MipMaps, we must assert npot2!
    // glGenerateMipmap(GL_TEXTURE_2D);
    // release and return texture
    glBindTexture(GL_TEXTURE_2D, 0);

//    log_info("width: %d, height: %d, type: %d, pixels: %p", width, height, type, pixels);

    return texture_object_id;
}
/* externed */ 
vec2 tex_size; // last loaded png size as (w, h)
/// textures
std::mutex textures_mtx;
GLuint load_png_asset_into_texture(const char* relative_path)
{  
    std::lock_guard<std::mutex> lock(textures_mtx);
    int w = 0, h = 0, comp= 0;
    unsigned char* image = stbi_load(relative_path, &w, &h, &comp, STBI_rgb_alpha);
    if(image == nullptr){
        log_debug( "%s(%s) FAILED!", __FUNCTION__, relative_path);
        return 0;
    }

    GLuint tex = load_texture(w, h, comp, image);
    log_info("loaded %s(%s) as %d", __FUNCTION__, relative_path, tex);
    stbi_image_free(image);
    return tex;
}

void check_tex_for_reload(int idx)
{
    std::string cb_path;
    std::lock_guard<std::mutex> lock(disc_lock);
    int w, h;
    cb_path = fmt::format("{}/{}.png", APP_PATH("covers"), all_apps[idx].info.id);

    if (!if_exists(cb_path.c_str()) || !is_png_vaild(cb_path.c_str(), &w, &h))
    {
        log_info("%s doesnt exist", cb_path.c_str());
        all_apps[idx].icon.cover_exists = false;
    }
    else
        all_apps[idx].icon.cover_exists = true;
}

void check_n_load_textures(int idx)
{
    std::lock_guard<std::mutex> lock(disc_lock);
    int w, h;

    if (all_apps[idx].icon.texture.load() == GL_NULL)
    {
        std::string cb_path = fmt::format("{}/{}.png", APP_PATH("covers"), all_apps[idx].info.id);
        if (all_apps[idx].icon.cover_exists)
            all_apps[idx].icon.texture = load_png_asset_into_texture(cb_path.c_str());
        else // load icon0 if cover doesnt exist
            all_apps[idx].icon.texture = load_png_asset_into_texture(all_apps[idx].info.picpath.c_str());
    }
}

static void check_n_draw_textures(int idx, int SH_type, vec4 col)
{

   // std::lock_guard<std::mutex> lock(disc_lock);

    cover_t.render_tex(cb_tex, SH_type, col);

    if (!all_apps[idx].icon.cover_exists && all_apps[idx].icon.texture == GL_NULL)
         cover_i.render_tex(fallback_t, SH_type, col);
    else
         cover_i.render_tex(all_apps[idx].icon.texture.load(), SH_type, col);

//   log_info("tex %d", all_apps[idx].icon.texture.load() );
}

static void LoadIconsAsync(int i){
    vec4 colo = (1.);
    uint64_t fmem = 0;
    check_tex_for_reload(i);
    check_n_load_textures(i);
}

 std::vector<std::future<void>> m_future;
 int i = 0;
 for (item_t &item : all_apps)
 {
     m_future.push_back(std::async(std::launch::async, LoadIconsAsync, i));
     i++;
 }

called in the main loop

check_n_draw_textures(tex_idx, +1, colo);

I tried to make it more thread-safe by using mutexs but later I read something about sharing GL contexts


Solution

  • OpenGL is a state machine; if it were thread safe in the simplest manner, then statements like:

    glBindTexture(GL_TEXTURE_2D, texture_object_id);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    

    would not be possible because for all you know some other thread interceded and bound a different texture in between your bind and setting the texture parameters, or resumed and set some other parameters after you'd bound the texture.

    If using EGL, what you need to do is create an additional context for your secondary thread, specifying your main context as the share_context. Then each thread will have its own complete version of the OpenGL state machine but will share texture IDs, etc.

    Load your texture on the secondary thread and make sure you glFinish after setting it up so that any changes you've made which OpenGL is dealing with asynchronously are complete.

    Then hand that texture ID back to the main context for use.