如何在其他线程上创建纹理?

时间:2019-02-10 20:05:47

标签: c++ multithreading opengl textures

我想在程序执行其他操作时创建纹理。

我知道如何在不同的线程上渲染我的场景,但是我不知道如何在不同的线程上创建纹理然后在另一个线程上渲染它。

此刻,我得到了一个渲染红色四边形并更改背景颜色的测试场景

class TestApp
{
public:
    TestApp()
    {
        //Create sdl window 
        window.Create("window", 800, 600, WindowFlag_OpenGL);

        //add attributes (have not got my own impl yet)
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 6);
        SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

        //Shared context!!!
        SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);

        //Create context
        //SDL_GL_MakeCurrent(...) is called inside
        context = Context::Create(window);

        glewExperimental = true;
        glewInit();
    }

    ~TestApp()
    {
        drawThread->join();
    }

    void MainLoop()
    {
        drawThread= std::make_unique<std::thread>([&]() {
            //Create draw context
            //SDL_GL_MakeCurrent(...) is called inside
            drawContext = Context::Create(window);

            //simple batched renderer (textures, quads, triangles etc.)
            renderer2D.Create(window);

            while (!quit) {
               //I am more into 0 - 255 than 0.0f - 1.0f with colors
                Renderer::ClearColor(sinf(SDL_GetTicks() / 500.0f) * 255.0f, 0.0f, 255.0f, 255.0f); 
                //by default it clears color, stencil and depth buffer bit
                Renderer::ClearBuffers();

                //Draw red rect
                renderer2D.RenderClear();
                renderer2D->FillRect({ 0.0f, 0.0f, 50.0f, 50.0f }, 0xff0000ff);
                renderer2D.RenderPresent();

                //SDL_GL_SwapWindows();
                window.SwapWindows();
            }
        });

        while (!quit) {
            while (SDL_PollEvent(&event)) {
                if (event.type == SDL_QUIT) {
                    quit = true;
                }
            }
        }
    }

    std::unique_ptr<Context> drawContext;

    std::unique_ptr<std::thread> drawThread= nullptr;
    std::mutex mutex;
};

现在,我想在新线程中创建纹理,假设textureThread,然后在drawThread中进行渲染。

要创建纹理,我有一个名为Texture的类和静态方法Create(const std::string& path);

所以,让我们开始

我必须创建textureThread线程;

std::unique_ptr<std::thread> textureThread = nullptr;

和纹理

std::unique_ptr<Texture> texture = nullptr;

和一种加载纹理的新方法

void LoadTexture()
{
    textureThread = std::make_unique([&]() {
        std::lock_quard<std::mutex> guard(mutex);
        //Create draw context
        //SDL_GL_MakeCurrent(...) is called inside
        drawContext = Context::Create(window);

        texture = Texture::Create("Textures/test.png");
    });
}

现在我有2个问题:

  • 我应该创建一个新的上下文还是仅使用drawContext
  • 我应该SwapWindows() LoadTexture()的结尾吗?

前进

主循环应该几乎没有什么变化

void MainLoop()
{
    LoadTexture();

    drawThread= std::make_unique<std::thread>([&]() {
           std::lock_quard<std::mutex> guard(mutex);
           //...

            while (!quit) {
                //...

                //Draw texture
                renderer2D.RenderClear();
                renderer2D->Draw(texture, {0.0f, 0.0f, 50.0f, 50.0f});
                renderer2D.RenderPresent();

                //...
            }
        });
}

让我们认为我们将使用上面的代码运行该应用程序 当我运行它时,出现GL_INVALID_OPERATION: glBindTexture错误,表示

GL_INVALID_OPERATION is generated if texture was previously created with a target that doesn't match that of target.,但这不是事实。

要在单独的线程中加载纹理,我还需要什么?

编辑:

所以我需要: -创建一个新对象std::unique_ptr<Context> textureContext = nullptr; -创建当前的drawContext,然后创建textureContext -创建纹理 -在绘图线程中使用该纹理

像这样:

void LoadTexture()
{
    textureThread = std::make_unique<std::thread>([&]() {
        std::unique_lock<std::mutex> locker(mutex);
        //if draw context is not created we cannot make it current, so just 
        //check if it is already created and make it current.
        if (drawContext.get()) {
            drawContext->MakeCurrent();
            textureContext = Context::Create(window);
            texture = Texture::LoadPNG("Textures/test.png");
        }
    });
}

void MainLoop()
{
    LoadTexture();
    drawThread = std::make_unique<std::thread>([&]() {
        std::unique_lock<std::mutex> locker(mutex);
        drawContext = Context::Create(window);
        drawContext->MakeCurrent();

        renderer2D.Create(window);

        while (!quit) {
            drawContext->MakeCurrent();
            Renderer::RenderBackgroundColor(sinf(SDL_GetTicks() / 500.0f) * 255.0f, 0.0f, 255.0f, 255.0f);
            Renderer::ClearBuffers();

            renderer2D.RenderClear();
            renderer2D->Draw(texture, { 0.0f, 0.0f, 50.0f, 50.0f });
            renderer2D.RenderPresent();

            window.SwapWindows();
        }
    });

    while (!quit) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                quit = true;
            }
        }
    }
}

Context::Create()现在不呼叫SDL_GL_MakeCurrent()。我必须手动调用它。

使用这种方法,我摆脱了GL_INVALID_OPERATION: glBindTexture错误,但是渲染器(而不是纹理)渲染黑色四边形,即使具有可渲染的纹理(不是130MB且分辨率非常高)。

编辑

我认为这样的线程安全队列应该可以:

template<typename T>
class ThreadSafeQueue
{
public:
    ThreadSafeQueue() {}
    ~ThreadSafeQueue() {}

    void Push(T d)
    {
        std::unique_lock<std::mutex> locker(mutex);

        data.push(std::move(d));

        locker.unlock();

        cond.notify_one();
    }

    void get(T& res)
    {
        std::unique_lock<std::mutex> locker(mutex);

        cond.wait(locker, [=]() { return stop || !data.empty(); });

        if (stop && data.empty()) {
            return;
        }

        res = std::move(data.front());
        data.pop();
    }

    std::queue<T> data;

    std::mutex mutex;
    std::condition_variable cond;
    bool stop = false;
};

我的纹理类有一个新的对象和方法:

ThreadSafeQueue<SDL_Surface*> queue;
void LoadingThread(const std::string& path) 
{
    SDL_Surface* surf = std::async(std::launch::async, IMG_Load, path.c_str()).get();
    queue.Push(surf);
}

void Texture::LoadFromFile(const std::string& path)
{
    LoadingThread(path);
    SDL_Surface* surface = nullptr;

    if (!queue.data.empty() && path != "") {
        queue.get(surface);
        w = surface->w;
        h = surface->h;
        pixels = surface->pixels;
    } else return;

    glCall(glGenTextures(1, &texture));
    glCall(glBindTexture(GL_TEXTURE_2D, texture));

    glCall(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels));

    glCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT));
    glCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));
    glCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
    glCall(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));

    glCall(glGenerateMipmap(GL_TEXTURE_2D));

    glCall(glBindTexture(GL_TEXTURE_2D, 0));
}

void Draw() 
{
    renderThread = std::make_unique<std::thread>([&]() {
        drawContext->MakeCurrent();

        renderer2D.Create(window);

        std::unique_ptr<Texture> texture = Texture::LoadFromFile("textures/text.png");

        while (!quit) {
            Renderer::RenderBackgroundColor(sinf(SDL_GetTicks() / 500.0f) * 255.0f, 0.0f, 255.0f, 255.0f);
            Renderer::ClearBuffers();

            renderer2D.RenderClear();
            renderer2D->Draw(texture, { 0.0f, 0.0f, 50.0f, 50.0f });
            renderer2D.RenderPresent();

            window.SwapWindows();
        }

    });
}

但是我想我做错了,程序会停止运行直到加载纹理

2 个答案:

答案 0 :(得分:2)

我过去这样做的方法是在线程中创建一个新的OpenGL上下文,并确保它与您用来绘制的上下文位于同一共享组中。我尚未在Windows上使用SDL或OpenGL,因此我不知道您用于创建新上下文或指定共享组的确切功能,但是看起来您可以使用wglShareLists()

但是接下来的问题是,为什么要在另一个线程上创建纹理?请记住,GPU一次只能处理一件事情。它既可以创建纹理,也可以设置和处理渲染中的命令缓冲区。这不会因为您在另一个线程上创建纹理而改变。如果尝试在渲染线程正在渲染时创建纹理,则它可能只是阻塞正在渲染线程上完成的工作,或被其阻塞。但是,如果渲染线程在OpenGL渲染和CPU计算之间交替,那么在另一个线程上创建纹理仍然有意义。但是在不同的线程上执行操作不会使它突然发生更快。

编辑:

鉴于您要加载130MB的纹理,建议您重新考虑一下您的工作方式。假设每个像素为32位,则类似于8k x 4k。当前没有支持该分辨率的监视器。这意味着您要么在任何给定时间只显示纹理的一部分,要么以缩小的比例显示纹理。因此,上传整个纹理没有任何意义。

您可能想要做的是平铺纹理,使用mipmap或同时使用两者。平铺将使您一次只能上传纹理的一小部分。如果要以全分辨率显示它,则只需上传对用户可见的部分。

Mipmapping提取纹理并制作尺寸缩小的副本,该副本的宽度和高度为1/2,宽度和高度为1/4,宽度和高度为1/8,等等。如果要显示整个纹理,那么您只需要一个或两个mipmap级别即可显示它,这将占用更少的内存。

我建议您执行其中一项或两项操作,而不要尝试创建和使用单个130MB纹理。您也可能会尝试在低端硬件上达到尺寸限制,尝试创建8k x 4k的纹理。

编辑2:

如上所述,您需要在2个上下文之间共享。在696行的SDL_windowsopengl.c中查看SDL的源代码,我们看到,如果gl_config的{​​{1}}标志设置为true,则将share_with_current_context设置为当前上下文。由于我不了解SDL,因此我不确定要设置为打开该标志必须设置什么标志,但这就是您需要做的。创建渲染上下文,将标志设置为true,然后创建纹理上下文。然后,它们将共享纹理,并且在纹理线程上创建的任何纹理都应在渲染线程上可用。

答案 1 :(得分:1)

必须在最后创建主上下文。您可以在主线程中创建renderContexttextureContext,但是在创建主上下文之前,必须创建它们。在我看来,创建renderContexttextureContext上下文的顺序无关紧要(我可能错了)。共享上下文的标志与您已设置的标志相同:SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);

换句话说:

window.Create("window", 800, 600, WindowFlag_OpenGL);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 6);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);

textureContext = Context::Create(window);
drawContext = Context::Create(window);

//I tried with this order and it does the same
//drawContext = Context::Create(window);
//textureContext = Context::Create(window);

//Main context is created at the end!!!!
context = Context::Create(window);


// init glew or whatever you wanna 
...

我认为您想等到纹理加载后再渲染它,所以我添加了一些东西

SDL_atomic_t stuffIsReady;

void LoadTexture()
{
    textureThread = std::make_unique<std::thread>([&]() {
        std::unique_lock<std::mutex> locker(mutex);
        textureContext->MakeCurrent();
        texture = Texture::LoadPNG("Media/image");

        //It is very important in shared contexts to make sure the driver is done with all Objects before signaling other threads that they can use them! 
        GLsync fenceId = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
        GLenum result = 0;
        while (true)
        {
            glClientWaitSync(fenceId, GL_SYNC_FLUSH_COMMANDS_BIT, GLuint64(5000000000)); //5 Second timeout 
            if (result != GL_TIMEOUT_EXPIRED) break; //we ignore timeouts and wait until all OpenGL commands are processed! 
        }

        SDL_AtomicIncRef(&stuffIsReady);
    });
}


void Draw() 
{
    drawThread = std::make_unique<std::thread>([&]() {
        drawContext->MakeCurrent();

        renderer2D.Create(window);

        while (!quit) {
            Renderer::RenderBackgroundColor(sinf(SDL_GetTicks() / 500.0f) * 255.0f, 0.0f, 255.0f, 255.0f);
            Renderer::ClearBuffers();

            renderer2D.RenderClear();
            if (SDL_AtomicGet(&stuffIsReady) == 1) {
                renderer2D->Draw(texture, { 0.0f, 0.0f, 50.0f, 50.0f });
            }
            renderer2D.RenderPresent();

            window.SwapWindows();
        }

    });
}

void MainLoop()
{
    LoadTexture();
    Draw();

    while (!quit) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                quit = true;
            }
        }
    }
}

最大的问题是最初创建的主要上下文。