Skip to content

Commit 786c5cd

Browse files
committed
Refactor material caching and resource initialization to improve performance, reduce redundant lookups, and enhance thread safety in rendering pipelines.
Hopefully fix windows, also, fix frames-in-flight so we can handle more than 1. Hopefully, improve the windows frame rate. In linux testing all rendering types and options result in about 60fps on an older computer. Forward+ does drop to 30, but that's due to requirements of the Forward+ methodology.
1 parent 606be30 commit 786c5cd

File tree

9 files changed

+818
-422
lines changed

9 files changed

+818
-422
lines changed

attachments/simple_engine/engine.cpp

Lines changed: 135 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,37 @@ Engine::Engine() :
3333
{
3434
}
3535

36+
bool Engine::IsMainThread() const
37+
{
38+
return std::this_thread::get_id() == mainThreadId;
39+
}
40+
41+
void Engine::ProcessPendingEntityRemovals()
42+
{
43+
std::vector<std::string> names;
44+
{
45+
std::lock_guard<std::mutex> lk(pendingEntityRemovalsMutex);
46+
if (pendingEntityRemovalNames.empty())
47+
return;
48+
names.swap(pendingEntityRemovalNames);
49+
}
50+
51+
// Process on the main thread only (safety)
52+
if (!IsMainThread())
53+
{
54+
// Put them back; we'll retry next main-thread tick
55+
std::lock_guard<std::mutex> lk(pendingEntityRemovalsMutex);
56+
pendingEntityRemovalNames.insert(pendingEntityRemovalNames.end(), names.begin(), names.end());
57+
return;
58+
}
59+
60+
// Apply removals using the normal API (which takes the appropriate locks).
61+
for (const auto &name : names)
62+
{
63+
(void) RemoveEntity(name);
64+
}
65+
}
66+
3667
Engine::~Engine()
3768
{
3869
Cleanup();
@@ -46,6 +77,9 @@ bool Engine::Initialize(const std::string &appName, int width, int height, bool
4677
// This will be handled in the android_main function
4778
return false;
4879
#else
80+
// Record main thread identity for deferring destructive operations from background threads
81+
mainThreadId = std::this_thread::get_id();
82+
4983
platform = CreatePlatform();
5084
if (!platform->Initialize(appName, width, height))
5185
{
@@ -188,8 +222,11 @@ void Engine::Cleanup()
188222
}
189223

190224
// Clear entities
191-
entities.clear();
192-
entityMap.clear();
225+
{
226+
std::unique_lock<std::shared_mutex> lk(entitiesMutex);
227+
entities.clear();
228+
entityMap.clear();
229+
}
193230

194231
// Clean up subsystems in reverse order of creation
195232
imguiSystem.reset();
@@ -205,6 +242,7 @@ void Engine::Cleanup()
205242

206243
Entity *Engine::CreateEntity(const std::string &name)
207244
{
245+
std::unique_lock<std::shared_mutex> lk(entitiesMutex);
208246
// Always allow duplicate names; map stores a representative entity
209247
// Create the entity
210248
auto entity = std::make_unique<Entity>(name);
@@ -219,6 +257,7 @@ Entity *Engine::CreateEntity(const std::string &name)
219257

220258
Entity *Engine::GetEntity(const std::string &name)
221259
{
260+
std::shared_lock<std::shared_mutex> lk(entitiesMutex);
222261
auto it = entityMap.find(name);
223262
if (it != entityMap.end())
224263
{
@@ -234,6 +273,17 @@ bool Engine::RemoveEntity(Entity *entity)
234273
return false;
235274
}
236275

276+
// If called from a background thread, defer removal to avoid deleting entities
277+
// while the render thread may be iterating a snapshot.
278+
if (!IsMainThread())
279+
{
280+
std::lock_guard<std::mutex> lk(pendingEntityRemovalsMutex);
281+
pendingEntityRemovalNames.push_back(entity->GetName());
282+
return true;
283+
}
284+
285+
std::unique_lock<std::shared_mutex> lk(entitiesMutex);
286+
237287
// Remember the name before erasing ownership
238288
std::string name = entity->GetName();
239289

@@ -271,12 +321,50 @@ bool Engine::RemoveEntity(Entity *entity)
271321

272322
bool Engine::RemoveEntity(const std::string &name)
273323
{
274-
Entity *entity = GetEntity(name);
275-
if (entity)
324+
// If called from a background thread, defer removal to avoid deleting entities
325+
// while the render thread may be iterating a snapshot.
326+
if (!IsMainThread())
276327
{
277-
return RemoveEntity(entity);
328+
std::lock_guard<std::mutex> lk(pendingEntityRemovalsMutex);
329+
pendingEntityRemovalNames.push_back(name);
330+
return true;
278331
}
279-
return false;
332+
333+
std::unique_lock<std::shared_mutex> lk(entitiesMutex);
334+
auto it = entityMap.find(name);
335+
if (it == entityMap.end())
336+
return false;
337+
Entity *entity = it->second;
338+
if (!entity)
339+
return false;
340+
341+
// Find the entity in the vector
342+
auto vecIt = std::ranges::find_if(entities,
343+
[entity](const std::unique_ptr<Entity> &e) {
344+
return e.get() == entity;
345+
});
346+
if (vecIt == entities.end())
347+
{
348+
entityMap.erase(name);
349+
return false;
350+
}
351+
352+
entities.erase(vecIt);
353+
354+
// Update the map: point to another entity with the same name if one exists
355+
auto remainingIt = std::ranges::find_if(entities,
356+
[&name](const std::unique_ptr<Entity> &e) {
357+
return e && e->GetName() == name;
358+
});
359+
if (remainingIt != entities.end())
360+
{
361+
entityMap[name] = remainingIt->get();
362+
}
363+
else
364+
{
365+
entityMap.erase(name);
366+
}
367+
return true;
280368
}
281369

282370
void Engine::SetActiveCamera(CameraComponent *cameraComponent)
@@ -441,6 +529,9 @@ void Engine::handleKeyInput(uint32_t key, bool pressed)
441529

442530
void Engine::Update(TimeDelta deltaTime)
443531
{
532+
// Apply any entity removals requested by background threads.
533+
ProcessPendingEntityRemovals();
534+
444535
// During background scene loading we avoid touching the live entity
445536
// list from the main thread. This lets the loading thread construct
446537
// entities/components safely while the main thread only drives the
@@ -478,17 +569,23 @@ void Engine::Update(TimeDelta deltaTime)
478569
UpdateCameraControls(deltaTime);
479570
}
480571

481-
// Update all entities (guard against null unique_ptrs)
482-
for (auto &entity : entities)
572+
// Update all entities.
573+
// Do not hold `entitiesMutex` while calling `Entity::Update()`.
574+
// Background threads may need the unique lock to add entities during loading,
575+
// and holding a shared lock for a long time can starve them.
576+
std::vector<Entity *> snapshot;
483577
{
484-
if (!entity)
578+
std::shared_lock<std::shared_mutex> lk(entitiesMutex);
579+
snapshot.reserve(entities.size());
580+
for (auto &uptr : entities)
485581
{
486-
continue;
582+
snapshot.push_back(uptr.get());
487583
}
488-
if (!entity->IsActive())
489-
{
584+
}
585+
for (Entity *entity : snapshot)
586+
{
587+
if (!entity || !entity->IsActive())
490588
continue;
491-
}
492589
entity->Update(deltaTime);
493590
}
494591
}
@@ -507,8 +604,24 @@ void Engine::Render()
507604
return;
508605
}
509606

607+
// Apply any entity removals requested by background threads before taking a snapshot.
608+
ProcessPendingEntityRemovals();
609+
610+
// Snapshot entity pointers under a short shared lock, then release the lock
611+
// before rendering. This prevents starving the background loader/physics threads
612+
// that need the unique lock to create entities/components.
613+
std::vector<Entity *> snapshot;
614+
{
615+
std::shared_lock<std::shared_mutex> lk(entitiesMutex);
616+
snapshot.reserve(entities.size());
617+
for (auto &uptr : entities)
618+
{
619+
snapshot.push_back(uptr.get());
620+
}
621+
}
622+
510623
// Render the scene (ImGui will be rendered within the render pass)
511-
renderer->Render(entities, activeCamera, imguiSystem.get());
624+
renderer->Render(snapshot, activeCamera, imguiSystem.get());
512625
}
513626

514627
std::chrono::milliseconds Engine::CalculateDeltaTimeMs()
@@ -574,8 +687,14 @@ void Engine::UpdateCameraControls(TimeDelta deltaTime)
574687
if (imguiSystem && imguiSystem->IsCameraTrackingEnabled())
575688
{
576689
// Find the first active ball entity
577-
auto ballEntityIt = std::ranges::find_if(entities, [](auto const &entity) { return entity->IsActive() && (entity->GetName().find("Ball_") != std::string::npos); });
578-
Entity *ballEntity = ballEntityIt != entities.end() ? ballEntityIt->get() : nullptr;
690+
Entity *ballEntity = nullptr;
691+
{
692+
std::shared_lock<std::shared_mutex> lk(entitiesMutex);
693+
auto ballEntityIt = std::ranges::find_if(entities, [](auto const &entity) {
694+
return entity && entity->IsActive() && (entity->GetName().find("Ball_") != std::string::npos);
695+
});
696+
ballEntity = (ballEntityIt != entities.end()) ? ballEntityIt->get() : nullptr;
697+
}
579698

580699
if (ballEntity)
581700
{

attachments/simple_engine/engine.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818

1919
#include <chrono>
2020
#include <memory>
21+
#include <mutex>
22+
#include <shared_mutex>
2123
#include <string>
24+
#include <thread>
2225
#include <unordered_map>
2326
#include <vector>
2427

@@ -218,9 +221,23 @@ class Engine
218221
std::unique_ptr<ImGuiSystem> imguiSystem;
219222

220223
// Entities
224+
// NOTE: Entities can be created from a background loading thread (see `main.cpp`).
225+
// Protect the containers to avoid iterator invalidation/data races while the render thread
226+
// iterates them.
227+
mutable std::shared_mutex entitiesMutex;
221228
std::vector<std::unique_ptr<Entity>> entities;
222229
std::unordered_map<std::string, Entity *> entityMap;
223230

231+
// Main thread identity (used to defer destructive operations from background threads)
232+
std::thread::id mainThreadId{};
233+
234+
// Background threads may request entity removal while the render thread is iterating snapshots.
235+
// To keep `Entity*` snapshots safe, defer removals to the main thread at a safe point.
236+
std::mutex pendingEntityRemovalsMutex;
237+
std::vector<std::string> pendingEntityRemovalNames;
238+
void ProcessPendingEntityRemovals();
239+
bool IsMainThread() const;
240+
224241
// Active camera
225242
CameraComponent *activeCamera = nullptr;
226243

0 commit comments

Comments
 (0)