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