Home > Net >  How to implement 1 or 2 frames lag between the main and the render thread?
How to implement 1 or 2 frames lag between the main and the render thread?

Time:10-06

I've read that engines skip 1 or 2 frames and keep this distance to ensure that the render thread and the main thread won't go too much forward.

I've got a very simple command queue that allows the main thread to queue commands and the render thread to dispatch them, but I don't know how I can keep 1/2 frames distance between these threads.

basic implementation:

#include <iostream>
#include <queue>
#include <mutex>
#include <functional>
#include <thread>
#include <condition_variable>

struct CommandQueue
{
    //not thread-safe
    //called only by the main thread
    //collects all gl calls from the main thread
    void Submit(std::function<void()> command)
    {
        commands.push(std::move(command));
    }

    //called only by the main thread
    //when the current frame has finished pushing gl commands
    //we're ready to push them into the render thread
    void Flush()
    {
        { 
            std::unique_lock<std::mutex> lock(mutex);
            commandsToExecute = std::move(commands);
        }

        cv.notify_one();
    }

    //called only by the render thread
    //submit gl calls from our queue into the graphics queue
    bool Execute()
    {
        auto renderCommands = WaitForCommands();
        if(renderCommands.empty()) {
            return false;
        }

        while(!renderCommands.empty()) {
            auto cmd = std::move(renderCommands.front());
            renderCommands.pop();

            cmd();
        }

        return true;
    }

    void Quit()
    {
        quit.store(true, std::memory_order_relaxed);
        cv.notify_one();
    }

private:
    std::queue<std::function<void()>> WaitForCommands()
    {
        std::unique_lock<std::mutex> lock(mutex);
        cv.wait(lock, [this]() { return !commandsToExecute.empty() || quit.load(std::memory_order_relaxed); });

        auto result = std::move(commandsToExecute);
        return result;
    }

    std::mutex mutex;
    std::condition_variable cv;
    std::queue<std::function<void()>> commands;
    std::queue<std::function<void()>> commandsToExecute;

    std::atomic_bool quit{false};
};

int main()
{
    CommandQueue commandQueue;

    std::thread renderThread([&](){
        while(true) {
            if(!commandQueue.Execute()) {
                break;
            }
        }
    });

    bool quit = false;
    while(!quit) {
        //example commands...
        commandQueue.Submit([](){
            glClear(GL_COLOR_BUFFER_BIT);
        });
        commandQueue.Submit([](){
            glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
        });

        commandQueue.Submit([](){
            glViewport(0, 0, 1600, 900);
        });

        commandQueue.Submit([](){
            SDL_GL_SwapWindow(window);
        });

        //notify the render thread that there is work to be done
        commandQueue.Flush();
    }

    commandQueue.Flush();
    commandQueue.Quit();
    renderThread.join();

    return 0;
}

How can I implement this 1/2 frames lag?

CodePudding user response:

I've found Filament Engine and it has FrameSkipper class which implements the solution I need.

quick example:

//what class should implement Tick functionality?
std::vector<std::function<bool()>> tickFunctions;

void RunAndRemove()
{
    auto it = tickFunctions.begin();
    while (it != tickFunctions.end()) {
        if ((*it)()) {
            it = tickFunctions.erase(it);
        }
        else   it;
    }
}

void Tick(CommandQueue& queue)
{
    queue.Submit([]() { RunAndRemove(); });
}

struct Fence
{
    Fence(CommandQueue& queue)
            : queue(queue)
    {
        queue.Submit([this]() {
            fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);

            std::weak_ptr<Status> weak = result;
            tickFunctions.push_back([this, weak]() {
                auto result = weak.lock();
                if (result) {
                    GLenum status = glClientWaitSync(fence, 0, 0u);
                    result->store(status, std::memory_order_relaxed);

                    return (status != GL_TIMEOUT_EXPIRED);
                }
                return true;
            });
        });
    }

    ~Fence()
    {
        queue.Submit([fence = fence]() {
            glDeleteSync(fence);
        });
    }

    GLenum getFenceStatus()
    {
        if (!result) {
            return GL_TIMEOUT_EXPIRED;
        }

        return result->load();
    }

    GLsync fence = nullptr;

    using Status = std::atomic<GLenum>;
    std::shared_ptr<Status> result{ std::make_shared<Status>(GL_TIMEOUT_EXPIRED) };
    CommandQueue& queue;
};

struct FrameSkipper
{
    FrameSkipper(CommandQueue& queue, size_t latency = 1)
            : queue(queue), last(latency)
    {
    }

    ~FrameSkipper() = default;

    bool Begin()
    {
        auto& syncs = delayedSyncs;
        auto sync = syncs.front();
        if (sync) {
            auto status = sync->getFenceStatus();
            if (status == GL_TIMEOUT_EXPIRED) {
                // Sync not ready, skip frame
                return false;
            }
            sync.reset();
        }
        // shift all fences down by 1
        std::move(syncs.begin()   1, syncs.end(), syncs.begin());
        syncs.back() = {};
        return true;
    }

    void End()
    {
        auto& sync = delayedSyncs[last];
        if (sync) {
            sync.reset();
        } sync = std::make_shared<Fence>(queue);
    }

private:
    static constexpr size_t MAX_FRAME_LATENCY = 4;

    using Container = std::array<std::shared_ptr<Fence>, MAX_FRAME_LATENCY>;
    mutable Container delayedSyncs{};

    CommandQueue& queue;

    size_t last;
};
int main()
{
    CommandQueue commandQueue;

    std::thread renderThread([&](){
        while(true) {
            if(!commandQueue.Execute()) {
                break;
            }
        }
    });

    FrameSkipper frameSkipper(commandQueue);

    auto BeginFrame = [&]() {
        Tick(commandQueue);

        if(frameSkipper.Begin()) {
            return true;
        }

        commandQueue.Flush();
        return false;
    };


    auto EndFrame = [&]() {
        frameSkipper.End();

        Tick(commandQueue);
        commandQueue.Flush();
    };

    bool quit = false;
    while(!quit) {
        if(BeginFrame()) {
            commandQueue.Submit([](){
                glClear(GL_COLOR_BUFFER_BIT);
            });
            commandQueue.Submit([](){
                glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
            });

            commandQueue.Submit([](){
                glViewport(0, 0, 1600, 900);
            });

            commandQueue.Submit([](){
                SDL_GL_SwapWindow(window);
            });

            EndFrame();
        }
    }

    commandQueue.Flush();
    commandQueue.Quit();
    renderThread.join();

    return 0;
}
  • Related