diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp index 673701cac4..e9a78dccde 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp @@ -38,19 +38,20 @@ namespace AzFramework void SpawnableEntitiesContainer::SpawnAllEntities() { AZ_Assert(m_threadData, "Calling SpawnAllEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket); + SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default); } void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector entityIndices) { AZ_Assert(m_threadData, "Calling SpawnEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->SpawnEntities(m_threadData->m_spawnedEntitiesTicket, AZStd::move(entityIndices)); + SpawnableEntitiesInterface::Get()->SpawnEntities( + m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default, AZStd::move(entityIndices)); } void SpawnableEntitiesContainer::DespawnAllEntities() { AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket); + SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default); } void SpawnableEntitiesContainer::Reset(AZ::Data::Asset spawnable) @@ -66,7 +67,9 @@ namespace AzFramework m_monitor.Disconnect(); m_monitor.m_threadData.reset(); - SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket, + SpawnableEntitiesInterface::Get()->Barrier( + m_threadData->m_spawnedEntitiesTicket, + SpawnablePriorty_Default, [threadData = m_threadData](EntitySpawnTicket&) mutable { threadData.reset(); @@ -83,7 +86,9 @@ namespace AzFramework void SpawnableEntitiesContainer::Alert(AlertCallback callback) { AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket, + SpawnableEntitiesInterface::Get()->Barrier( + m_threadData->m_spawnedEntitiesTicket, + SpawnablePriorty_Default, [generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket&) { callback(generation); @@ -110,6 +115,7 @@ namespace AzFramework AZ_Assert(m_threadData, "SpawnableEntitiesContainer is monitoring a spawnable, but doesn't have the associated data."); AZ_TracePrintf("Spawnables", "Reloading spawnable '%s'.\n", replacementAsset.GetHint().c_str()); - SpawnableEntitiesInterface::Get()->ReloadSpawnable(m_threadData->m_spawnedEntitiesTicket, AZStd::move(replacementAsset)); + SpawnableEntitiesInterface::Get()->ReloadSpawnable( + m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default, AZStd::move(replacementAsset)); } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h index 69bca8e111..97d06e1f37 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -24,6 +25,14 @@ namespace AZ namespace AzFramework { + AZ_TYPE_SAFE_INTEGRAL(SpawnablePriority, uint8_t); + + inline static constexpr SpawnablePriority SpawnablePriorty_Highest { 0 }; + inline static constexpr SpawnablePriority SpawnablePriorty_High { 32 }; + inline static constexpr SpawnablePriority SpawnablePriorty_Default { 128 }; + inline static constexpr SpawnablePriority SpawnablePriorty_Low { 192 }; + inline static constexpr SpawnablePriority SpawnablePriorty_Lowest { 255 }; + class SpawnableEntityContainerView { public: @@ -124,10 +133,10 @@ namespace AzFramework SpawnableIndexEntityIterator m_end; }; - //! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that be used as a template. A ticket can - //! be reused for multiple calls on the same spawnable and is safe to use by multiple threads at the same time. Entities created + //! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that is used as a template. A ticket can + //! be reused for multiple calls on the same spawnable and is safe to be used by multiple threads at the same time. Entities created //! from the spawnable may be tracked by the ticket and so using the same ticket is needed to despawn the exact entities created - //! by a call so spawn entities. The life cycle of the spawned entities is tied to the ticket and all entities spawned using a + //! by a call to spawn entities. The life cycle of the spawned entities is tied to the ticket and all entities spawned using a //! ticket will be despawned when it's deleted. class EntitySpawnTicket { @@ -159,10 +168,19 @@ namespace AzFramework using BarrierCallback = AZStd::function; //! Interface definition to (de)spawn entities from a spawnable into the game world. + //! //! While the callbacks of the individual calls are being processed they will block processing any other request. Callbacks can be //! issued from threads other than the one that issued the call, including the main thread. + //! //! Calls on the same ticket are guaranteed to be executed in the order they are issued. Note that when issuing requests from //! multiple threads on the same ticket the order in which the requests are assigned to the ticket is not guaranteed. + //! + //! Most calls have a priority where values closer to 0 mean higher priority than values closer to 255. The implementation of this + //! interface may choose to use priority lanes which doesn't guarantee that higher priority requests happen before lower priority + //! requests if they don't pass the priority lane threshold. Priority lanes and their thresholds are implementation specific and may + //! differ between platforms. Note that if a call happened on a ticket with lower priority followed by a one with a higher priority + //! the first lower priority call will still needs to complete before the second higher priority call can be executed and the priority + //! of the first call will not be updated. class SpawnableEntitiesDefinition { public: @@ -173,40 +191,48 @@ namespace AzFramework virtual ~SpawnableEntitiesDefinition() = default; //! Spawn instances of all entities in the spawnable. - //! @param spawnable The Spawnable asset that will be used to create entity instances from. //! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them. + //! @param priority The priority at which this call will be executed. //! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from //! a different thread than the one that made the function call. The returned list of entities contains all the newly //! created entities. - virtual void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {}, + virtual void SpawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) = 0; //! Spawn instances of some entities in the spawnable. //! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them. + //! @param priority The priority at which this call will be executed. //! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from. //! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from //! a different thread than the one that made this function call. The returned list of entities contains all the newly //! created entities. - virtual void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector entityIndices, + virtual void SpawnEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector entityIndices, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) = 0; //! Removes all entities in the provided list from the environment. //! @param ticket The ticket previously used to spawn entities with. + //! @param priority The priority at which this call will be executed. //! @param completionCallback Optional callback that's called when despawning entities has completed. This can be called from //! a different thread than the one that made this function call. - virtual void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) = 0; + virtual void DespawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback = {}) = 0; //! Removes all entities in the provided list from the environment and reconstructs the entities from the provided spawnable. - //! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them. + //! @param ticket Holds the information on the entities to reload. + //! @param priority The priority at which this call will be executed. //! @param spawnable The spawnable that will replace the existing spawnable. Both need to have the same asset id. //! @param completionCallback Optional callback that's called when the entities have been reloaded. This can be called from //! a different thread than the one that made this function call. The returned list of entities contains all the replacement //! entities. - virtual void ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, + virtual void ReloadSpawnable( + EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset spawnable, ReloadSpawnableCallback completionCallback = {}) = 0; //! List all entities that are spawned using this ticket. //! @param ticket Only the entities associated with this ticket will be listed. + //! @param priority The priority at which this call will be executed. //! @param listCallback Required callback that will be called to list the entities on. - virtual void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) = 0; + virtual void ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) = 0; //! List all entities that are spawned using this ticket with their spawnable index. //! Spawnables contain a flat list of entities, which are used as templates to spawn entities from. For every spawned entity //! the index of the entity in the spawnable that was used as a template is stored. This version of ListEntities will return @@ -214,17 +240,23 @@ namespace AzFramework //! the same index may appear multiple times as there are no restriction on how many instance of a specific entity can be //! created. //! @param ticket Only the entities associated with this ticket will be listed. + //! @param priority The priority at which this call will be executed. //! @param listCallback Required callback that will be called to list the entities and indices on. - virtual void ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) = 0; + virtual void ListIndicesAndEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) = 0; //! Claim all entities that are spawned using this ticket. Ownership of the entities is transferred from the ticket to the //! caller through the callback. After this call the ticket will have no entities associated with it. The caller of //! this function will need to manage the entities after this call. //! @param ticket Only the entities associated with this ticket will be released. + //! @param priority The priority at which this call will be executed. //! @param listCallback Required callback that will be called to transfer the entities through. - virtual void ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) = 0; + virtual void ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) = 0; //! Blocks until all operations made on the provided ticket before the barrier call have completed. - virtual void Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback) = 0; + //! @param ticket The ticket to monitor. + //! @param priority The priority at which this call will be executed. + //! @param completionCallback Required callback that will be called as soon as the barrier has been reached. + virtual void Barrier(EntitySpawnTicket& ticket, SpawnablePriority priority, BarrierCallback completionCallback) = 0; //! Register a handler for OnSpawned events. virtual void AddOnSpawnedHandler(AZ::Event>::Handler& handler) = 0; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 7e20f7b265..959d2ab64f 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -22,7 +22,19 @@ namespace AzFramework { - void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback, + template + void SpawnableEntitiesManager::QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request) + { + Queue& queue = priority <= HighPriorityThreshold ? m_highPriorityQueue : m_regularPriorityQueue; + { + AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex); + request.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; + queue.m_pendingRequest.push(AZStd::move(request)); + } + } + + void SpawnableEntitiesManager::SpawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback) { AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnAllEntities hasn't been initialized."); @@ -31,15 +43,11 @@ namespace AzFramework queueEntry.m_ticket = &ticket; queueEntry.m_completionCallback = AZStd::move(completionCallback); queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } void SpawnableEntitiesManager::SpawnEntities( - EntitySpawnTicket& ticket, AZStd::vector entityIndices, + EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector entityIndices, EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback) { AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized."); @@ -49,28 +57,22 @@ namespace AzFramework queueEntry.m_entityIndices = AZStd::move(entityIndices); queueEntry.m_completionCallback = AZStd::move(completionCallback); queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback) + void SpawnableEntitiesManager::DespawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback) { AZ_Assert(ticket.IsValid(), "Ticket provided to DespawnAllEntities hasn't been initialized."); DespawnAllEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_completionCallback = AZStd::move(completionCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, + void SpawnableEntitiesManager::ReloadSpawnable( + EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset spawnable, ReloadSpawnableCallback completionCallback) { AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized."); @@ -79,14 +81,10 @@ namespace AzFramework queueEntry.m_ticket = &ticket; queueEntry.m_spawnable = AZStd::move(spawnable); queueEntry.m_completionCallback = AZStd::move(completionCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) + void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) { AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use."); AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized."); @@ -94,14 +92,11 @@ namespace AzFramework ListEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_listCallback = AZStd::move(listCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) + void SpawnableEntitiesManager::ListIndicesAndEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) { AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use."); AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized."); @@ -109,14 +104,10 @@ namespace AzFramework ListIndicesEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_listCallback = AZStd::move(listCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) + void SpawnableEntitiesManager::ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) { AZ_Assert(listCallback, "ClaimEntities called on spawnable entities without a valid callback to use."); AZ_Assert(ticket.IsValid(), "Ticket provided to ClaimEntities hasn't been initialized."); @@ -124,14 +115,10 @@ namespace AzFramework ClaimEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_listCallback = AZStd::move(listCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback) + void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, SpawnablePriority priority, BarrierCallback completionCallback) { AZ_Assert(completionCallback, "Barrier on spawnable entities called without a valid callback to use."); AZ_Assert(ticket.IsValid(), "Ticket provided to Barrier hasn't been initialized."); @@ -139,11 +126,7 @@ namespace AzFramework BarrierCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_completionCallback = AZStd::move(completionCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } void SpawnableEntitiesManager::AddOnSpawnedHandler(AZ::Event>::Handler& handler) @@ -156,34 +139,54 @@ namespace AzFramework handler.Connect(m_onDespawnedEvent); } - auto SpawnableEntitiesManager::ProcessQueue() -> CommandQueueStatus + auto SpawnableEntitiesManager::ProcessQueue(CommandQueuePriority priority) -> CommandQueueStatus + { + CommandQueueStatus result = CommandQueueStatus::NoCommandsLeft; + if ((priority & CommandQueuePriority::High) == CommandQueuePriority::High) + { + if (ProcessQueue(m_highPriorityQueue) == CommandQueueStatus::HasCommandsLeft) + { + result = CommandQueueStatus::HasCommandsLeft; + } + } + if ((priority & CommandQueuePriority::Regular) == CommandQueuePriority::Regular) + { + if (ProcessQueue(m_regularPriorityQueue) == CommandQueueStatus::HasCommandsLeft) + { + result = CommandQueueStatus::HasCommandsLeft; + } + } + return result; + } + + auto SpawnableEntitiesManager::ProcessQueue(Queue& queue) -> CommandQueueStatus { AZStd::queue pendingRequestQueue; { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - m_pendingRequestQueue.swap(pendingRequestQueue); + AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex); + queue.m_pendingRequest.swap(pendingRequestQueue); } - if (!pendingRequestQueue.empty() || !m_delayedQueue.empty()) + if (!pendingRequestQueue.empty() || !queue.m_delayed.empty()) { AZ::SerializeContext* serializeContext = nullptr; AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext); AZ_Assert(serializeContext, "Failed to retrieve serialization context."); // Only process the requests that are currently in this queue, not the ones that could be re-added if they still can't complete. - size_t delayedSize = m_delayedQueue.size(); + size_t delayedSize = queue.m_delayed.size(); for (size_t i = 0; i < delayedSize; ++i) { - Requests& request = m_delayedQueue.front(); + Requests& request = queue.m_delayed.front(); bool result = AZStd::visit([this, serializeContext](auto&& args) -> bool { return ProcessRequest(args, *serializeContext); }, request); if (!result) { - m_delayedQueue.emplace_back(AZStd::move(request)); + queue.m_delayed.emplace_back(AZStd::move(request)); } - m_delayedQueue.pop_front(); + queue.m_delayed.pop_front(); } do @@ -197,7 +200,7 @@ namespace AzFramework }, request); if (!result) { - m_delayedQueue.emplace_back(AZStd::move(request)); + queue.m_delayed.emplace_back(AZStd::move(request)); } pendingRequestQueue.pop(); } @@ -205,13 +208,13 @@ namespace AzFramework // Spawning entities can result in more entities being queued to spawn. Repeat spawning until the queue is // empty to avoid a chain of entity spawning getting dragged out over multiple frames. { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - m_pendingRequestQueue.swap(pendingRequestQueue); + AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex); + queue.m_pendingRequest.swap(pendingRequestQueue); } } while (!pendingRequestQueue.empty()); } - return m_delayedQueue.empty() ? CommandQueueStatus::NoCommandLeft : CommandQueueStatus::HasCommandsLeft; + return queue.m_delayed.empty() ? CommandQueueStatus::NoCommandsLeft : CommandQueueStatus::HasCommandsLeft; } void* SpawnableEntitiesManager::CreateTicket(AZ::Data::Asset&& spawnable) @@ -226,9 +229,9 @@ namespace AzFramework DestroyTicketCommand queueEntry; queueEntry.m_ticket = reinterpret_cast(ticket); { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); + AZStd::scoped_lock queueLock(m_regularPriorityQueue.m_pendingRequestMutex); queueEntry.m_ticketId = reinterpret_cast(ticket)->m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); + m_regularPriorityQueue.m_pendingRequest.push(AZStd::move(queueEntry)); } } diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index 3481ab180a..137376ac7b 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -29,8 +29,6 @@ namespace AZ namespace AzFramework { - using EntityIdMap = AZStd::unordered_map; - class SpawnableEntitiesManager : public SpawnableEntitiesInterface::Registrar { @@ -38,31 +36,48 @@ namespace AzFramework AZ_RTTI(AzFramework::SpawnableEntitiesManager, "{6E14333F-128C-464C-94CA-A63B05A5E51C}"); AZ_CLASS_ALLOCATOR(SpawnableEntitiesManager, AZ::SystemAllocator, 0); + using EntityIdMap = AZStd::unordered_map; + enum class CommandQueueStatus : bool { HasCommandsLeft, - NoCommandLeft + NoCommandsLeft }; + enum class CommandQueuePriority + { + High = 1 << 0, + Regular = 1 << 1 + }; + + static constexpr SpawnablePriority HighPriorityThreshold = SpawnablePriority { 64 }; + ~SpawnableEntitiesManager() override = default; // // The following functions are thread safe // - void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) override; - void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector entityIndices, EntityPreInsertionCallback preInsertionCallback = {}, + void SpawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) override; - void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) override; + void SpawnEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector entityIndices, + EntityPreInsertionCallback preInsertionCallback = {}, + EntitySpawnCallback completionCallback = {}) override; + void DespawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback = {}) override; - void ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, + void ReloadSpawnable( + EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset spawnable, ReloadSpawnableCallback completionCallback = {}) override; - void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) override; - void ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) override; - void ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) override; + void ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) override; + void ListIndicesAndEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) override; + void ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) override; - void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback) override; + void Barrier(EntitySpawnTicket& spawnInfo, SpawnablePriority priority, BarrierCallback completionCallback) override; void AddOnSpawnedHandler(AZ::Event>::Handler& handler) override; void AddOnDespawnedHandler(AZ::Event>::Handler& handler) override; @@ -71,13 +86,9 @@ namespace AzFramework // The following function is thread safe but intended to be run from the main thread. // - CommandQueueStatus ProcessQueue(); + CommandQueueStatus ProcessQueue(CommandQueuePriority priority); protected: - void* CreateTicket(AZ::Data::Asset&& spawnable) override; - void DestroyTicket(void* ticket) override; - - private: struct Ticket { AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0); @@ -153,6 +164,20 @@ namespace AzFramework SpawnAllEntitiesCommand, SpawnEntitiesCommand, DespawnAllEntitiesCommand, ReloadSpawnableCommand, ListEntitiesCommand, ListIndicesEntitiesCommand, ClaimEntitiesCommand, BarrierCommand, DestroyTicketCommand>; + struct Queue + { + AZStd::deque m_delayed; //!< Requests that were processed before, but couldn't be completed. + AZStd::queue m_pendingRequest; //!< Requests waiting to be processed for the first time. + AZStd::mutex m_pendingRequestMutex; + }; + + template + void QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request); + void* CreateTicket(AZ::Data::Asset&& spawnable) override; + void DestroyTicket(void* ticket) override; + + CommandQueueStatus ProcessQueue(Queue& queue); + AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext); @@ -174,11 +199,12 @@ namespace AzFramework [[nodiscard]] static bool IsEqualTicket(const EntitySpawnTicket* lhs, const Ticket* rhs); [[nodiscard]] static bool IsEqualTicket(const Ticket* lhs, const Ticket* rhs); - AZStd::deque m_delayedQueue; //!< Requests that were processed before, but couldn't be completed. - AZStd::queue m_pendingRequestQueue; - AZStd::mutex m_pendingRequestQueueMutex; + Queue m_highPriorityQueue; + Queue m_regularPriorityQueue; AZ::Event> m_onSpawnedEvent; AZ::Event> m_onDespawnedEvent; }; + + AZ_DEFINE_ENUM_BITWISE_OPERATORS(AzFramework::SpawnableEntitiesManager::CommandQueuePriority); } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp index 262f006f15..32cc61914e 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp @@ -48,10 +48,23 @@ namespace AzFramework void SpawnableSystemComponent::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) { - m_entitiesManager.ProcessQueue(); + m_entitiesManager.ProcessQueue( + SpawnableEntitiesManager::CommandQueuePriority::High | SpawnableEntitiesManager::CommandQueuePriority::Regular); RootSpawnableNotificationBus::ExecuteQueuedEvents(); } + int SpawnableSystemComponent::GetTickOrder() + { + return AZ::ComponentTickBus::TICK_GAME; + } + + void SpawnableSystemComponent::OnSystemTick() + { + // Handle only high priority spawning events such as those created from network. These need to happen even if the server + // doesn't have focus to avoid + m_entitiesManager.ProcessQueue(SpawnableEntitiesManager::CommandQueuePriority::High); + } + void SpawnableSystemComponent::OnCatalogLoaded([[maybe_unused]] const char* catalogFile) { if (!m_catalogAvailable) @@ -168,7 +181,8 @@ namespace AzFramework SpawnableEntitiesManager::CommandQueueStatus queueStatus; do { - queueStatus = m_entitiesManager.ProcessQueue(); + queueStatus = m_entitiesManager.ProcessQueue( + SpawnableEntitiesManager::CommandQueuePriority::High | SpawnableEntitiesManager::CommandQueuePriority::Regular); } while (queueStatus == SpawnableEntitiesManager::CommandQueueStatus::HasCommandsLeft); } diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h index b29fbea5e4..1549b86385 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h @@ -28,6 +28,7 @@ namespace AzFramework class SpawnableSystemComponent : public AZ::Component , public AZ::TickBus::Handler + , public AZ::SystemTickBus::Handler , public AssetCatalogEventBus::Handler , public RootSpawnableInterface::Registrar , public RootSpawnableNotificationBus::Handler @@ -58,6 +59,13 @@ namespace AzFramework // void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; + int GetTickOrder() override; + + // + // SystemTickBus + // + + void OnSystemTick() override; // // AssetCatalogEventBus diff --git a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index 1fa50fff52..191b14c31d 100644 --- a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -55,7 +55,11 @@ namespace UnitTest delete m_ticket; m_ticket = nullptr; // One more tick on the spawnable entities manager in order to delete the ticket fully. - m_manager->ProcessQueue(); + while (m_manager->ProcessQueue( + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High | + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular) != + AzFramework::SpawnableEntitiesManager::CommandQueueStatus::NoCommandsLeft) + ; delete m_spawnableAsset; m_spawnableAsset = nullptr; @@ -96,8 +100,8 @@ namespace UnitTest { spawnedEntitiesCount += entities.size(); }; - m_manager->SpawnAllEntities(*m_ticket, {}, AZStd::move(callback)); - m_manager->ProcessQueue(); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, {}, AZStd::move(callback)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_EQ(NumEntities, spawnedEntitiesCount); } @@ -119,9 +123,9 @@ namespace UnitTest spawnedEntitiesCount += entities.size(); }; - m_manager->SpawnAllEntities(*m_ticket); - m_manager->ListEntities(*m_ticket, AZStd::move(callback)); - m_manager->ProcessQueue(); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default); + m_manager->ListEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_TRUE(allValidEntityIds); EXPECT_EQ(NumEntities, spawnedEntitiesCount); @@ -148,11 +152,73 @@ namespace UnitTest } }; - m_manager->SpawnAllEntities(*m_ticket); - m_manager->ListIndicesAndEntities(*m_ticket, AZStd::move(callback)); - m_manager->ProcessQueue(); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default); + m_manager->ListIndicesAndEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_TRUE(allValidEntityIds); EXPECT_EQ(NumEntities, spawnedEntitiesCount); } + + TEST_F(SpawnableEntitiesManagerTest, Priority_HighBeforeDefault_HigherPriorityCallHappensBeforeDefaultPriorityEvenWhenQueuedLater) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + AzFramework::EntitySpawnTicket highPriorityTicket(*m_spawnableAsset); + + size_t callCounter = 1; + size_t highPriorityCallId = 0; + size_t defaultPriorityCallId = 0; + auto highCallback = [&callCounter, &highPriorityCallId] + (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) + { + highPriorityCallId = callCounter++; + }; + auto defaultCallback = [&callCounter, &defaultPriorityCallId] + (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) + { + defaultPriorityCallId = callCounter++; + }; + + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, {}, AZStd::move(defaultCallback)); + m_manager->SpawnAllEntities(highPriorityTicket, AzFramework::SpawnablePriorty_High, {}, AZStd::move(highCallback)); + m_manager->ProcessQueue( + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High | + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_LT(highPriorityCallId, defaultPriorityCallId); + } + + TEST_F(SpawnableEntitiesManagerTest, Priority_SameTicket_DefaultPriorityCallHappensBeforeHighPriority) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + size_t callCounter = 1; + size_t highPriorityCallId = 0; + size_t defaultPriorityCallId = 0; + auto highCallback = + [&callCounter, &highPriorityCallId](AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) + { + highPriorityCallId = callCounter++; + }; + auto defaultCallback = + [&callCounter, &defaultPriorityCallId](AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) + { + defaultPriorityCallId = callCounter++; + }; + + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, {}, AZStd::move(defaultCallback)); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_High, {}, AZStd::move(highCallback)); + m_manager->ProcessQueue( + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High | + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + // Run a second time as the high priority task will be pending at this point. + m_manager->ProcessQueue( + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High | + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_LT(defaultPriorityCallId, highPriorityCallId); + } } // namespace UnitTest