@@ -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+
3667Engine::~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
206243Entity *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
220258Entity *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
272322bool 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
282370void Engine::SetActiveCamera (CameraComponent *cameraComponent)
@@ -441,6 +529,9 @@ void Engine::handleKeyInput(uint32_t key, bool pressed)
441529
442530void 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
514627std::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 {
0 commit comments