Skip to content

Commit

Permalink
Restructure the engine update and rendering loops
Browse files Browse the repository at this point in the history
Restructure the engine update and rendering loops with the intention
to move things more forward a model where the "update" step can be
executed as a task in a separate thread and the time step used by
the update is then fed to the renderer in order to update the renderer
material animations etc.

The current blocker is that the renderer is still creating gfx
resources during the draw operation and accessess the entity/entity
node objects for this purpose. This would require locking and would
not produce the best parallelism. A refactoring should be done to
restructure the renderer so that no access to scene/entity/entity nodes
is needed during render.

Issue #195
  • Loading branch information
ensisoft committed Mar 7, 2024
1 parent c4e2842 commit 993ff2f
Showing 1 changed file with 82 additions and 84 deletions.
166 changes: 82 additions & 84 deletions engine/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,14 @@ class GameStudioEngine final : public engine::Engine,
}

TRACE_CALL("Renderer::BeginFrame", mRenderer.BeginFrame());
// Update the rendering state, animate materials and drawables.
TRACE_CALL("Renderer::Update", mRenderer.Update(mRenderTimeTotal, mRenderStep));
TRACE_CALL("Renderer::DrawScene", mRenderer.Draw(*mDevice, mTilemap.get()));
TRACE_CALL("Renderer::EndFrame", mRenderer.EndFrame());
TRACE_CALL("DebugDraw", DrawDebugObjects());

mRenderTimeTotal += mRenderStep;
mRenderStep = 0;
}

{
Expand Down Expand Up @@ -541,75 +546,21 @@ class GameStudioEngine final : public engine::Engine,
// do simulation/animation update steps.
while (mTimeAccum >= mGameTimeStep)
{
// current game time from which we step forward
// in ticks later on.
auto tick_time = mGameTimeTotal;

if (mScene)
{
TRACE_CALL("Scene::BeginLoop", mScene->BeginLoop());
TRACE_CALL("Runtime:BeginLoop", mRuntime->BeginLoop());
}

// Call UpdateGame with the *current* time. I.e. the game
// is advancing one time step from current mGameTimeTotal.
// this is consistent with the tick time accumulation below.
TRACE_CALL("UpdateGame", UpdateGame(mGameTimeTotal, mGameTimeStep));
mGameTimeTotal += mGameTimeStep;
mTimeAccum -= mGameTimeStep;
mTickAccum += mGameTimeStep;

// PostUpdate allows the game to perform activities with consistent
// world state after everything has settled down. It might be tempting
// to bake the functionality of "Rebuild" in the scene in the loop
// end functionality and let the game perform the "PostUpdate" actions in
// the Update function. But this has the problem that during the call
// to Update (on each entity instance) the world doesn't yet have
// consistent state because not every object that needs to move has
// moved. This might lead to incorrect conclusions when for exampling
// trying to detect whether things are colling/overlapping. For example
// if entity A's Update function updates A's position and checks whether
// A is hitting some other object those other objects may or may not have
// been moved already. To resolve this issue the game should move entity A
// in the Update function and then check for the collisions/overlap/whatever
// in the PostUpdate with consistent world state.
if (mScene)
{
// make sure to do this first in order to allow the scene to rebuild
// the spatial indices etc. before the game's PostUpdate runs.
TRACE_CALL("Scene::Rebuild", mScene->Rebuild());
// using the time we've arrived to now after having taken the previous
// delta step forward in game time.
TRACE_CALL("Runtime::PostUpdate", mRuntime->PostUpdate(mGameTimeTotal));
}

// todo: add mGame->PostUpdate here if needed
// TRACE_CALL("Game::PostUpdate", mGame->PostUpdate(mGameTimeTotal))

// It might be tempting to use the tick functionality to perform
// some type of movement in some types of games. For example
// a tetris-clone could move the pieces on tick steps instead of
// continuous updates. But to keep things simple the engine is only
// promising consistent world state to exist during the call to
// PostUpdate. In order to support a simple use case such as
// "move on tick" the game should set a flag on the Tick, perform
// move on update when the flag is set and then clear the flag.
while (mTickAccum >= mGameTickStep)
{
TRACE_CALL("Runtime::Tick", mRuntime->Tick(tick_time, mGameTickStep));
mTickAccum -= mGameTickStep;
tick_time += mGameTickStep;
}
TRACE_CALL("UpdateGameUI", UpdateGameUI(mGameTimeTotal, mGameTimeStep));

if (mScene)
{
TRACE_CALL("Runtime::EndLoop", mRuntime->EndLoop());
TRACE_CALL("Scene::EndLoop", mScene->EndLoop());
}
mGameTimeTotal += mGameTimeStep;
mTimeAccum -= mGameTimeStep;

// if we're paused for debugging stop after one step forward.
mStepForward = false;
}
TRACE_CALL("GameActions", PerformGameActions(dt));

TRACE_CALL("HandleGameActions", PerformGameActions(dt));
}
virtual void EndMainLoop() override
{
Expand Down Expand Up @@ -1020,7 +971,8 @@ class GameStudioEngine final : public engine::Engine,
{
if (mScene)
{
TRACE_SCOPE("UpdateScene");
TRACE_CALL("Scene::BeginLoop", mScene->BeginLoop());
TRACE_CALL("Runtime:BeginLoop", mRuntime->BeginLoop());

std::vector<game::Scene::Event> events;
TRACE_CALL("Scene::Update", mScene->Update(dt, &events));
Expand All @@ -1041,39 +993,60 @@ class GameStudioEngine final : public engine::Engine,
// dispatch the contact events (if any).
TRACE_CALL("Runtime::OnContactEvents", mRuntime->OnContactEvent(contacts));
}

// Update renderers data structures from the scene.
// This involves creating new render nodes for new entities
// that have been spawned etc.
TRACE_CALL("Renderer::UpdateState", mRenderer.UpdateRenderStateFromScene(*mScene));
// Update the rendering state, animate materials and drawables.
TRACE_CALL("Renderer::Update", mRenderer.Update(game_time, dt));
}

TRACE_CALL("Runtime::Update", mRuntime->Update(game_time, dt));

// Update the UI system
// Tick game
{
TRACE_SCOPE("UpdateUI");

std::vector<engine::UIEngine::WidgetAction> widget_actions;
std::vector<engine::UIEngine::WindowAction> window_actions;
TRACE_CALL("UIEngine::UpdateWindow", mUIEngine.UpdateWindow(game_time, dt, &widget_actions));
TRACE_CALL("UIEngine::UpdateState", mUIEngine.UpdateState(game_time, dt, &window_actions));
TRACE_CALL("Runtime::OnUIAction", mRuntime->OnUIAction(mUIEngine.GetUI(), widget_actions));
mTickAccum += dt;
// current game time from which we step forward
// in ticks later on.
auto tick_time = game_time;

for (const auto& w : window_actions)
while (mTickAccum >= mGameTickStep)
{
if (const auto* ptr = std::get_if<engine::UIEngine::WindowOpen>(&w))
mRuntime->OnUIOpen(ptr->window);
else if (const auto* ptr = std::get_if<engine::UIEngine::WindowUpdate>(&w))
mRuntime->SetCurrentUI(ptr->window);
else if(const auto* ptr = std::get_if<engine::UIEngine::WindowClose>(&w))
mRuntime->OnUIClose(ptr->window.get(), ptr->result);
else BUG("Missing UIEngine window event handling.");
TRACE_CALL("Runtime::Tick", mRuntime->Tick(tick_time, mGameTickStep));
mTickAccum -= mGameTickStep;
tick_time += mGameTickStep;
}
}

// PostUpdate allows the game to perform activities with consistent
// world state after everything has settled down. It might be tempting
// to bake the functionality of "Rebuild" in the scene in the loop
// end functionality and let the game perform the "PostUpdate" actions in
// the Update function. But this has the problem that during the call
// to Update (on each entity instance) the world doesn't yet have
// consistent state because not every object that needs to move has
// moved. This might lead to incorrect conclusions when for exampling
// trying to detect whether things are colling/overlapping. For example
// if entity A's Update function updates A's position and checks whether
// A is hitting some other object those other objects may or may not have
// been moved already. To resolve this issue the game should move entity A
// in the Update function and then check for the collisions/overlap/whatever
// in the PostUpdate with consistent world state.
if (mScene)
{
// Update renderers data structures from the scene.
// This involves creating new render nodes for new entities
// that have been spawned etc. This needs to be done inside
// the begin/end loop in order to have the correct singalling
// i.e. entity control flags.
TRACE_CALL("Renderer::UpdateState", mRenderer.UpdateRenderStateFromScene(*mScene));
mRenderStep += dt;

// make sure to do this first in order to allow the scene to rebuild
// the spatial indices etc. before the game's PostUpdate runs.
TRACE_CALL("Scene::Rebuild", mScene->Rebuild());
// using the time we've arrived to now after having taken the previous
// delta step forward in game time.
TRACE_CALL("Runtime::PostUpdate", mRuntime->PostUpdate(game_time + dt));

TRACE_CALL("Runtime::EndLoop", mRuntime->EndLoop());
TRACE_CALL("Scene::EndLoop", mScene->EndLoop());
}

// update mouse pointer
{
gfx::Drawable::Environment env; // todo:
Expand All @@ -1082,6 +1055,26 @@ class GameStudioEngine final : public engine::Engine,
}
}

void UpdateGameUI(double game_time, float dt)
{
std::vector<engine::UIEngine::WidgetAction> widget_actions;
std::vector<engine::UIEngine::WindowAction> window_actions;
TRACE_CALL("UIEngine::UpdateWindow", mUIEngine.UpdateWindow(game_time, dt, &widget_actions));
TRACE_CALL("UIEngine::UpdateState", mUIEngine.UpdateState(game_time, dt, &window_actions));
TRACE_CALL("Runtime::OnUIAction", mRuntime->OnUIAction(mUIEngine.GetUI(), widget_actions));

for (const auto& w : window_actions)
{
if (const auto* ptr = std::get_if<engine::UIEngine::WindowOpen>(&w))
mRuntime->OnUIOpen(ptr->window);
else if (const auto* ptr = std::get_if<engine::UIEngine::WindowUpdate>(&w))
mRuntime->SetCurrentUI(ptr->window);
else if(const auto* ptr = std::get_if<engine::UIEngine::WindowClose>(&w))
mRuntime->OnUIClose(ptr->window.get(), ptr->result);
else BUG("Missing UIEngine window event handling.");
}
}

void PerformGameActions(float dt)
{
// todo: the action processing probably needs to be split
Expand Down Expand Up @@ -1312,8 +1305,13 @@ class GameStudioEngine final : public engine::Engine,
// available for taking update and tick steps.
float mTickAccum = 0.0f;
float mTimeAccum = 0.0f;

float mRenderStep = 0.0;

// Total accumulated game time so far.
double mGameTimeTotal = 0.0f;
double mRenderTimeTotal = 0.0;

// The bitbag for storing game state.
engine::KeyValueStore mStateStore;
// debug stepping flag to control taking a single update step.
Expand Down

0 comments on commit 993ff2f

Please sign in to comment.