/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "Vegetation_precompiled.h" #include "AreaSystemComponent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef VEGETATION_EDITOR #include #endif #include #include #include namespace Vegetation { namespace AreaSystemUtil { template void hash_combine_64(uint64_t& seed, T const& v) { AZStd::hash hasher; seed ^= hasher(v) + 0x9e3779b97f4a7c13LL + (seed << 12) + (seed >> 4); } static bool UpdateVersion(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement) { if (classElement.GetVersion() < 4) { classElement.RemoveElementByName(AZ_CRC("ThreadSleepTimeMs", 0x9e86f79d)); } return true; } } ////////////////////////////////////////////////////////////////////////// // ViewRect inline bool AreaSystemComponent::ViewRect::IsInside(const SectorId& sector) const { const int inX = sector.first; const int inY = sector.second; return inX >= GetMinXSector() && inX <= GetMaxXSector() && inY >= GetMinYSector() && inY <= GetMaxYSector(); } AreaSystemComponent::ViewRect AreaSystemComponent::ViewRect::Overlap(const ViewRect& b) const { ViewRect o; o.m_x = m_x > b.m_x ? m_x : b.m_x; o.m_y = m_y > b.m_y ? m_y : b.m_y; o.m_width = m_x + m_width > b.m_x + b.m_width ? b.m_x + b.m_width : m_x + m_width; o.m_height = m_y + m_height > b.m_y + b.m_height ? b.m_y + b.m_height : m_y + m_height; o.m_width -= o.m_x; o.m_height -= o.m_y; return o; } bool AreaSystemComponent::ViewRect::operator==(const ViewRect& b) { return m_x == b.m_x && m_y == b.m_y && m_width == b.m_width && m_height == b.m_height; } size_t AreaSystemComponent::ViewRect::GetNumSectors() const { return static_cast(m_height * m_width); } bool AreaSystemComponent::ViewRect::operator!=(const ViewRect& b) { return m_x != b.m_x || m_y != b.m_y || m_width != b.m_width || m_height != b.m_height; } ////////////////////////////////////////////////////////////////////////// // DirtySectors void AreaSystemComponent::DirtySectors::MarkDirty(const SectorId& sector) { m_dirtySet.insert(AZStd::move(sector)); } void AreaSystemComponent::DirtySectors::MarkAllDirty() { m_allSectorsDirty = true; } void AreaSystemComponent::DirtySectors::Clear() { m_dirtySet.clear(); m_allSectorsDirty = false; } bool AreaSystemComponent::DirtySectors::IsDirty(const SectorId& sector) const { return m_allSectorsDirty || (!m_dirtySet.empty() && (m_dirtySet.find(sector) != m_dirtySet.end())); } ////////////////////////////////////////////////////////////////////////// // AreaSystemConfig // These limitations are somewhat arbitrary. It's possible to select combinations of larger values than these that will work successfully. // However, these values are also large enough that going beyond them is extremely likely to cause problems. const int AreaSystemConfig::s_maxViewRectangleSize = 128; const int AreaSystemConfig::s_maxSectorDensity = 64; const int AreaSystemConfig::s_maxSectorSizeInMeters = 1024; const int64_t AreaSystemConfig::s_maxVegetationInstances = 2 * 1024 * 1024; const int AreaSystemConfig::s_maxInstancesPerMeter = 16; void AreaSystemConfig::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serialize = azrtti_cast(context); if (serialize) { serialize->Class() ->Version(4, &AreaSystemUtil::UpdateVersion) ->Field("ViewRectangleSize", &AreaSystemConfig::m_viewRectangleSize) ->Field("SectorDensity", &AreaSystemConfig::m_sectorDensity) ->Field("SectorSizeInMeters", &AreaSystemConfig::m_sectorSizeInMeters) ->Field("ThreadProcessingIntervalMs", &AreaSystemConfig::m_threadProcessingIntervalMs) ->Field("SectorSearchPadding", &AreaSystemConfig::m_sectorSearchPadding) ->Field("SectorPointSnapMode", &AreaSystemConfig::m_sectorPointSnapMode) ; AZ::EditContext* edit = serialize->GetEditContext(); if (edit) { edit->Class( "Vegetation Area System Config", "Handles the placement and removal of vegetation instance based on the vegetation area component rules") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement(AZ::Edit::UIHandlers::Default, &AreaSystemConfig::m_viewRectangleSize, "View Area Grid Size", "The number of sectors (per-side) of a managed grid in a scrolling view centered around the camera.") ->Attribute(AZ::Edit::Attributes::ChangeValidate, &AreaSystemConfig::ValidateViewArea) ->Attribute(AZ::Edit::Attributes::Min, 1) ->Attribute(AZ::Edit::Attributes::Max, s_maxViewRectangleSize) ->DataElement(AZ::Edit::UIHandlers::Default, &AreaSystemConfig::m_sectorDensity, "Sector Point Density", "The number of equally-spaced vegetation instance grid placement points (per-side) within a sector") ->Attribute(AZ::Edit::Attributes::ChangeValidate, &AreaSystemConfig::ValidateSectorDensity) ->Attribute(AZ::Edit::Attributes::Min, 1) ->Attribute(AZ::Edit::Attributes::Max, s_maxSectorDensity) ->DataElement(AZ::Edit::UIHandlers::Default, &AreaSystemConfig::m_sectorSizeInMeters, "Sector Size In Meters", "The size in meters (per-side) of each sector.") ->Attribute(AZ::Edit::Attributes::ChangeValidate, &AreaSystemConfig::ValidateSectorSize) ->Attribute(AZ::Edit::Attributes::Min, 1) ->Attribute(AZ::Edit::Attributes::Max, s_maxSectorSizeInMeters) ->DataElement(AZ::Edit::UIHandlers::Default, &AreaSystemConfig::m_threadProcessingIntervalMs, "Thread Processing Interval", "The delay (in milliseconds) between processing queued thread tasks.") ->Attribute(AZ::Edit::Attributes::Min, 0) ->Attribute(AZ::Edit::Attributes::Max, 5000) ->DataElement(AZ::Edit::UIHandlers::Default, &AreaSystemConfig::m_sectorSearchPadding, "Sector Search Padding", "Increases the search radius for surrounding sectors when enumerating instances.") ->Attribute(AZ::Edit::Attributes::Min, 0) ->Attribute(AZ::Edit::Attributes::Max, 2) ->DataElement(AZ::Edit::UIHandlers::ComboBox, &AreaSystemConfig::m_sectorPointSnapMode, "Sector Point Snap Mode", "Controls whether vegetation placement points are located at the corner or the center of the cell.") ->EnumAttribute(SnapMode::Corner, "Corner") ->EnumAttribute(SnapMode::Center, "Center") ; } } if (auto behaviorContext = azrtti_cast(context)) { behaviorContext->Class() ->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::Preview) ->Attribute(AZ::Script::Attributes::Category, "Vegetation") ->Constructor() ->Property("viewRectangleSize", BehaviorValueProperty(&AreaSystemConfig::m_viewRectangleSize)) ->Property("sectorDensity", BehaviorValueProperty(&AreaSystemConfig::m_sectorDensity)) ->Property("sectorSizeInMeters", BehaviorValueProperty(&AreaSystemConfig::m_sectorSizeInMeters)) ->Property("threadProcessingIntervalMs", BehaviorValueProperty(&AreaSystemConfig::m_threadProcessingIntervalMs)) ->Property("sectorPointSnapMode", [](AreaSystemConfig* config) { return static_cast(config->m_sectorPointSnapMode); }, [](AreaSystemConfig* config, const AZ::u8& i) { config->m_sectorPointSnapMode = static_cast(i); }) ; } } AZ::Outcome AreaSystemConfig::ValidateViewArea(void* newValue, const AZ::Uuid& valueType) { if (azrtti_typeid() != valueType) { AZ_Assert(false, "Unexpected value type"); return AZ::Failure(AZStd::string("Unexpectedly received a non-int type for the View Area Grid Size!")); } int viewRectangleSize = *static_cast(newValue); const int instancesPerSector = m_sectorDensity * m_sectorDensity; const int totalSectors = viewRectangleSize * viewRectangleSize; int64_t totalInstances = instancesPerSector * totalSectors; if (totalInstances > s_maxVegetationInstances) { return AZ::Failure( AZStd::string::format("The combination of View Area Grid Size and Sector Point Density will create %" PRId64 " instances. Only a max of %" PRId64 " instances is allowed.", static_cast(totalInstances), static_cast(s_maxVegetationInstances))); } return AZ::Success(); } AZ::Outcome AreaSystemConfig::ValidateSectorDensity(void* newValue, const AZ::Uuid& valueType) { if (azrtti_typeid() != valueType) { AZ_Assert(false, "Unexpected value type"); return AZ::Failure(AZStd::string("Unexpectedly received a non-int type for the Sector Point Density!")); } int sectorDensity = *static_cast(newValue); const int instancesPerSector = sectorDensity * sectorDensity; const int totalSectors = m_viewRectangleSize * m_viewRectangleSize; int64_t totalInstances = instancesPerSector * totalSectors; if (totalInstances >= s_maxVegetationInstances) { return AZ::Failure( AZStd::string::format("The combination of View Area Grid Size and Sector Point Density will create %" PRId64 " instances. Only a max of %" PRId64 " instances is allowed.", static_cast(totalInstances), static_cast(s_maxVegetationInstances))); } const float instancesPerMeter = static_cast(sectorDensity) / static_cast(m_sectorSizeInMeters); if (instancesPerMeter > s_maxInstancesPerMeter) { return AZ::Failure(AZStd::string::format("The combination of Sector Point Density and Sector Size in Meters will create %.1f instances per meter. Only a max of %d instances per meter is allowed.", instancesPerMeter, s_maxInstancesPerMeter)); } return AZ::Success(); } AZ::Outcome AreaSystemConfig::ValidateSectorSize(void* newValue, const AZ::Uuid& valueType) { if (azrtti_typeid() != valueType) { AZ_Assert(false, "Unexpected value type"); return AZ::Failure(AZStd::string("Unexpectedly received a non-int type for the Sector Size In Meters!")); } int sectorSizeInMeters = *static_cast(newValue); const float instancesPerMeter = static_cast(m_sectorDensity) / static_cast(sectorSizeInMeters); if (instancesPerMeter > s_maxInstancesPerMeter) { return AZ::Failure(AZStd::string::format("The combination of Sector Point Density and Sector Size in Meters will create %.1f instances per meter. Only a max of %d instances per meter is allowed.", instancesPerMeter, s_maxInstancesPerMeter)); } return AZ::Success(); } ////////////////////////////////////////////////////////////////////////// // AreaSystemComponent void AreaSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) { services.push_back(AZ_CRC("VegetationAreaSystemService", 0x36da2b62)); } void AreaSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services) { services.push_back(AZ_CRC("VegetationAreaSystemService", 0x36da2b62)); } void AreaSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services) { services.push_back(AZ_CRC("VegetationDebugSystemService", 0x8cac3d67)); services.push_back(AZ_CRC("VegetationInstanceSystemService", 0x823a6007)); services.push_back(AZ_CRC("SurfaceDataSystemService", 0x1d44d25f)); } void AreaSystemComponent::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serialize = azrtti_cast(context); if (serialize) { serialize->Class() ->Version(0) ->Field("Configuration", &AreaSystemComponent::m_configuration) ; if (AZ::EditContext* editContext = serialize->GetEditContext()) { editContext->Class("Vegetation Area System", "Manages registration and processing of vegetation area entities") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "Vegetation") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("System", 0xc94d118b)) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://docs.aws.amazon.com/console/lumberyard/vegetation/vegetation-system-area") ->DataElement(0, &AreaSystemComponent::m_configuration, "Configuration", "") ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) ; } } AZ::BehaviorContext* behaviorContext = azrtti_cast(context); if (behaviorContext) { behaviorContext->EBus("AreaSystemRequestBus") ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation) ->Attribute(AZ::Script::Attributes::Category, "AreaSystem") ->Attribute(AZ::Script::Attributes::Module, "areasystem") ->Event("GetInstanceCountInAabb", &AreaSystemRequests::GetInstanceCountInAabb) ; } } AreaSystemComponent::AreaSystemComponent(const AreaSystemConfig& configuration) : m_configuration(configuration) { } void AreaSystemComponent::Init() { } void AreaSystemComponent::Activate() { // Wait for any lingering vegetation thread work to complete if necessary. (This should never actually occur) AZ_Assert(m_threadData.m_vegetationThreadState == PersistentThreadData::VegetationThreadState::Stopped, "Vegetation thread was still active even though AreaSystemComponent was deactivated."); AZStd::lock_guard lockTasks(m_threadData.m_vegetationThreadMutex); m_threadData.m_vegetationThreadState = PersistentThreadData::VegetationThreadState::Stopped; m_threadData.m_vegetationDataSyncState = PersistentThreadData::VegetationDataSyncState::Synchronized; m_system = GetISystem(); m_worldToSector = 1.0f / m_configuration.m_sectorSizeInMeters; // We initialize our vegetation threadData state here to ensure it gets recalculated the next time the thread runs. m_threadData.Init(); AZ::TickBus::Handler::BusConnect(); AreaSystemRequestBus::Handler::BusConnect(); GradientSignal::SectorDataRequestBus::Handler::BusConnect(); SystemConfigurationRequestBus::Handler::BusConnect(); InstanceStatObjEventBus::Handler::BusConnect(); CrySystemEventBus::Handler::BusConnect(); AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect(); SurfaceData::SurfaceDataSystemNotificationBus::Handler::BusConnect(); m_vegTasks.FetchDebugData(); } void AreaSystemComponent::Deactivate() { // Interrupt vegetation worker; deactivation deletes all vegetation, so there's no need to process updates. m_threadData.InterruptVegetationThread(); //wait for the vegetation thread work to complete AZStd::lock_guard lockTasks(m_threadData.m_vegetationThreadMutex); m_threadData.m_vegetationThreadState = PersistentThreadData::VegetationThreadState::Stopped; m_threadData.m_vegetationDataSyncState = PersistentThreadData::VegetationDataSyncState::Synchronized; AZ::TickBus::Handler::BusDisconnect(); AreaSystemRequestBus::Handler::BusDisconnect(); GradientSignal::SectorDataRequestBus::Handler::BusDisconnect(); SystemConfigurationRequestBus::Handler::BusDisconnect(); InstanceStatObjEventBus::Handler::BusDisconnect(); CrySystemEventBus::Handler::BusDisconnect(); AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect(); SurfaceData::SurfaceDataSystemNotificationBus::Handler::BusDisconnect(); // Clear sector data and any lingering vegetation thread state m_vegTasks.ClearSectors(); m_threadData.Init(); InstanceSystemRequestBus::Broadcast(&InstanceSystemRequestBus::Events::DestroyAllInstances); InstanceSystemRequestBus::Broadcast(&InstanceSystemRequestBus::Events::Cleanup); if (m_system) { m_system->GetISystemEventDispatcher()->RemoveListener(this); m_system = nullptr; } } bool AreaSystemComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) { if (auto config = azrtti_cast(baseConfig)) { m_configuration = *config; return true; } return false; } bool AreaSystemComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const { if (auto config = azrtti_cast(outBaseConfig)) { if (m_configDirty) { *config = m_pendingConfigUpdate; } else { *config = m_configuration; } return true; } return false; } void AreaSystemComponent::UpdateSystemConfig(const AZ::ComponentConfig* baseConfig) { if (const auto config = azrtti_cast(baseConfig)) { if ((!m_configDirty && m_configuration == *config) || (m_configDirty && m_pendingConfigUpdate == *config)) { return; } m_configDirty = true; m_pendingConfigUpdate = *config; } } void AreaSystemComponent::GetSystemConfig(AZ::ComponentConfig* outBaseConfig) const { WriteOutConfig(outBaseConfig); } bool AreaSystemComponent::ApplyPendingConfigChanges() { if (m_configDirty) { ReleaseWithoutCleanup(); if (m_configuration.m_threadProcessingIntervalMs != m_pendingConfigUpdate.m_threadProcessingIntervalMs) { m_vegetationThreadTaskTimer = 0.0f; } ReadInConfig(&m_pendingConfigUpdate); m_worldToSector = 1.0f / m_configuration.m_sectorSizeInMeters; RefreshAllAreas(); GradientSignal::SectorDataNotificationBus::Broadcast(&GradientSignal::SectorDataNotificationBus::Events::OnSectorDataConfigurationUpdated); m_configDirty = false; return true; } else { return false; } } void AreaSystemComponent::RegisterArea(AZ::EntityId areaId, AZ::u32 layer, AZ::u32 priority, const AZ::Aabb& bounds) { if (!bounds.IsValid()) { AZ_Assert(false, "Vegetation Area registered with an invalid AABB."); } m_vegTasks.QueueVegetationTask([areaId, layer, priority, bounds](UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks) { auto& area = threadData->m_globalVegetationAreaMap[areaId]; area.m_id = areaId; area.m_layer = layer; area.m_priority = priority; area.m_bounds = bounds; AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaRegistered); const auto& cachedMainThreadData = context->GetCachedMainThreadData(); vegTasks->MarkDirtySectors(area.m_bounds, threadData->m_dirtySectorContents, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); threadData->m_activeAreasDirty = true; }); } void AreaSystemComponent::UnregisterArea(AZ::EntityId areaId) { m_vegTasks.QueueVegetationTask([areaId](UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks) { auto itArea = threadData->m_globalVegetationAreaMap.find(areaId); if (itArea != threadData->m_globalVegetationAreaMap.end()) { const auto& area = itArea->second; AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaUnregistered); AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaDisconnect); const auto& cachedMainThreadData = context->GetCachedMainThreadData(); vegTasks->AddUnregisteredVegetationArea(area, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); vegTasks->MarkDirtySectors(area.m_bounds, threadData->m_dirtySectorContents, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); threadData->m_globalVegetationAreaMap.erase(itArea); threadData->m_activeAreasDirty = true; } }); } void AreaSystemComponent::RefreshArea(AZ::EntityId areaId, AZ::u32 layer, AZ::u32 priority, const AZ::Aabb& bounds) { if (!bounds.IsValid()) { AZ_Assert(false, "Vegetation Area refreshed with an invalid AABB."); } m_vegTasks.QueueVegetationTask([areaId, layer, priority, bounds](UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks) { auto itArea = threadData->m_globalVegetationAreaMap.find(areaId); if (itArea != threadData->m_globalVegetationAreaMap.end()) { auto& area = itArea->second; const auto& cachedMainThreadData = context->GetCachedMainThreadData(); vegTasks->MarkDirtySectors(area.m_bounds, threadData->m_dirtySectorContents, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); area.m_layer = layer; area.m_priority = priority; area.m_bounds = bounds; AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaRefreshed); vegTasks->MarkDirtySectors(area.m_bounds, threadData->m_dirtySectorContents, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); threadData->m_activeAreasDirty = true; } }); } void AreaSystemComponent::RefreshAllAreas() { m_vegTasks.QueueVegetationTask([](UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks) { for (auto& entry : threadData->m_globalVegetationAreaMap) { auto& area = entry.second; area.m_layer = {}; area.m_priority = {}; area.m_bounds = AZ::Aabb::CreateNull(); AreaInfoBus::EventResult(area.m_layer, area.m_id, &AreaInfoBus::Events::GetLayer); AreaInfoBus::EventResult(area.m_priority, area.m_id, &AreaInfoBus::Events::GetPriority); AreaInfoBus::EventResult(area.m_bounds, area.m_id, &AreaInfoBus::Events::GetEncompassingAabb); AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaRefreshed); } // Set all existing sectors as needing to be rebuilt. const auto& cachedMainThreadData = context->GetCachedMainThreadData(); vegTasks->MarkDirtySectors(AZ::Aabb::CreateNull(), threadData->m_dirtySectorContents, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); vegTasks->MarkDirtySectors(AZ::Aabb::CreateNull(), threadData->m_dirtySectorSurfacePoints, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); }); } void AreaSystemComponent::ClearAllAreas() { // Interrupt any work that's currently being done on the vegetation thread and destroy all vegetation instances ReleaseWithoutCleanup(); // Queue a refresh of all the areas RefreshAllAreas(); // Reset our timer for checking the vegetation queue for more work to ensure we process this immediately. m_vegetationThreadTaskTimer = 0.0f; } void AreaSystemComponent::MuteArea(AZ::EntityId areaId) { m_vegTasks.QueueVegetationTask([areaId](UpdateContext* /*context*/, PersistentThreadData* threadData, VegetationThreadTasks* /*vegTasks*/) { threadData->m_ignoredVegetationAreaSet.insert(areaId); threadData->m_activeAreasDirty = true; }); } void AreaSystemComponent::UnmuteArea(AZ::EntityId areaId) { m_vegTasks.QueueVegetationTask([areaId](UpdateContext* /*context*/, PersistentThreadData* threadData, VegetationThreadTasks* /*vegTasks*/) { threadData->m_ignoredVegetationAreaSet.erase(areaId); threadData->m_activeAreasDirty = true; }); } void AreaSystemComponent::OnSurfaceChanged(const AZ::EntityId& /*entityId*/, const AZ::Aabb& oldBounds, const AZ::Aabb& newBounds) { m_vegTasks.QueueVegetationTask([oldBounds, newBounds](UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks) { const auto& cachedMainThreadData = context->GetCachedMainThreadData(); // Mark the surface area prior to the surface data change as dirty vegTasks->MarkDirtySectors(oldBounds, threadData->m_dirtySectorContents, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); vegTasks->MarkDirtySectors(oldBounds, threadData->m_dirtySectorSurfacePoints, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); // Mark the surface area *after* the surface data change as dirty vegTasks->MarkDirtySectors(newBounds, threadData->m_dirtySectorContents, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); vegTasks->MarkDirtySectors(newBounds, threadData->m_dirtySectorSurfacePoints, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect); }); } void AreaSystemComponent::EnumerateInstancesInOverlappingSectors(const AZ::Aabb& bounds, AreaSystemEnumerateCallback callback) const { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); if (!bounds.IsValid()) { return; } // Get the minimum sector that overlaps the bounds, expanded outward based on sectorSearchPadding. const SectorId minSector = GetSectorId(bounds.GetMin(), m_worldToSector); AZ::Aabb minBounds = m_vegTasks.GetSectorBounds(SectorId(minSector.first - m_configuration.m_sectorSearchPadding, minSector.second - m_configuration.m_sectorSearchPadding), m_configuration.m_sectorSizeInMeters); // Get the maximum sector that overlaps the bounds, expanded outward based on sectorSearchPadding. const SectorId maxSector = GetSectorId(bounds.GetMax(), m_worldToSector); AZ::Aabb maxBounds = m_vegTasks.GetSectorBounds(SectorId(maxSector.first + m_configuration.m_sectorSearchPadding, maxSector.second + m_configuration.m_sectorSearchPadding), m_configuration.m_sectorSizeInMeters); // Use the expanded bounds to enumerate through all instances. AZ::Aabb expandedBounds(minBounds); expandedBounds.AddAabb(maxBounds); EnumerateInstancesInAabb(expandedBounds, callback); } void AreaSystemComponent::EnumerateInstancesInAabb(const AZ::Aabb& bounds, AreaSystemEnumerateCallback callback) const { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); if (!bounds.IsValid()) { return; } const SectorId minSector = GetSectorId(bounds.GetMin(), m_worldToSector); const int minX = minSector.first; const int minY = minSector.second; const SectorId maxSector = GetSectorId(bounds.GetMax(), m_worldToSector); const int maxX = maxSector.first; const int maxY = maxSector.second; // Lock the rolling window mutex for the entire enumerate to ensure that our set of sectors doesn't change during the loops. AZStd::lock_guard lock(m_vegTasks.m_sectorRollingWindowMutex); for (int currY = minY; currY <= maxY; ++currY) { for (int currX = minX; currX <= maxX; ++currX) { const SectorInfo* sectorInfo = m_vegTasks.GetSector(SectorId(currX, currY)); if (sectorInfo) // manual sector id's can be outside the active area { for (const auto& claimPair : sectorInfo->m_claimedWorldPoints) { const auto& instanceData = claimPair.second; if (bounds.Contains(instanceData.m_position)) { if (callback(instanceData) != AreaSystemEnumerateCallbackResult::KeepEnumerating) { return; } } } } } } } AZStd::size_t AreaSystemComponent::GetInstanceCountInAabb(const AZ::Aabb& bounds) const { AZStd::size_t result = 0; EnumerateInstancesInAabb(bounds, [&result](const auto&) { ++result; return AreaSystemEnumerateCallbackResult::KeepEnumerating; }); return result; } void AreaSystemComponent::GetPointsPerMeter(float& value) const { if (m_configuration.m_sectorDensity <= 0 || m_configuration.m_sectorSizeInMeters <= 0.0f) { value = 1.0f; } else { value = static_cast(m_configuration.m_sectorDensity) / m_configuration.m_sectorSizeInMeters; } } void AreaSystemComponent::OnTick(float deltaTime, AZ::ScriptTimePoint time) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); if (m_configuration.m_sectorSizeInMeters < 0) { m_configuration.m_sectorSizeInMeters = 1; } m_worldToSector = 1.0f / m_configuration.m_sectorSizeInMeters; m_vegetationThreadTaskTimer -= deltaTime; // Check to see if any vegetation data has changed since last tick, and if so, offload the updates to a vegetation thread. // - If the thread is currently stopped, check for data changes and start up the thread if changes are detected. // - If the thread has an interrupt requested, wait for the interrupt to stop the thread before checking and potentially running again. // - If the thread is currently running, only update if the data on the vegetation thread is currently synced with this thread. // If the state is currently Updating or Dirty, wait for the vegetation thread to pick up the changes before trying to update the // data again to avoid redundant mutex locks or the potential for mismatched state. if ((m_threadData.m_vegetationThreadState == PersistentThreadData::VegetationThreadState::Stopped) || ((m_threadData.m_vegetationThreadState == PersistentThreadData::VegetationThreadState::Running) && (m_threadData.m_vegetationDataSyncState == PersistentThreadData::VegetationDataSyncState::Synchronized))) { bool updateVegetationData = false; // if the config changes, we need to update the vegetation data if (ApplyPendingConfigChanges()) { updateVegetationData = true; } // if the view rectangle changes, we need to update the vegetation data if (CalculateViewRect()) { updateVegetationData = true; } if (m_vegetationThreadTaskTimer <= 0.0f) { m_vegetationThreadTaskTimer = m_configuration.m_threadProcessingIntervalMs * 0.001f; // If there are still vegetation tasks pending and we've waited the requested amount of time between queue checks, // then we need to update the vegetation data. if (m_vegTasks.VegetationThreadTasksPending()) { updateVegetationData = true; } } if (updateVegetationData) { // Our main thread has potentially updated its state, so cache a new copy of the pieces of state we need. { m_cachedMainThreadData.m_currViewRect = m_currViewRect; m_cachedMainThreadData.m_worldToSector = m_worldToSector; m_cachedMainThreadData.m_sectorSizeInMeters = m_configuration.m_sectorSizeInMeters; m_cachedMainThreadData.m_sectorDensity = m_configuration.m_sectorDensity; m_cachedMainThreadData.m_sectorPointSnapMode = m_configuration.m_sectorPointSnapMode; } // Set the state to Dirty to signal the thread that it will need to pull a new copy of the main thread state data // and refresh the set of work that it's currently doing. The thread will detect the change next time it looks // for work and clear the state after it pulls new data. m_threadData.m_vegetationDataSyncState = PersistentThreadData::VegetationDataSyncState::Dirty; // If the thread isn't currently running, start it up. if (m_threadData.m_vegetationThreadState == PersistentThreadData::VegetationThreadState::Stopped) { //create a job to process vegetation areas, tasks, sectors in the background m_threadData.m_vegetationThreadState = PersistentThreadData::VegetationThreadState::Running; auto job = AZ::CreateJobFunction([this]() { AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::Entity, "Vegetation::AreaSystemComponent::VegetationThread"); UpdateContext context; context.Run(&m_threadData, &m_vegTasks, &m_cachedMainThreadData); // After we're done processing as much as we can, clear our thread states and exit. m_threadData.m_vegetationThreadState = PersistentThreadData::VegetationThreadState::Stopped; m_threadData.m_vegetationDataSyncState = PersistentThreadData::VegetationDataSyncState::Synchronized; }, true); job->Start(); } } } } void AreaSystemComponent::OnTerrainDataCreateBegin() { // Interrupt any in-process updates until the next tick. We don't want to update // while terrain is being created, because we can end up with race conditions in // which we're querying terrain for some of the points while terrain is still only // partially created. This can happen during creation because the HeightmapModified // event fires mid-creation, which can block in TerrainSurfaceDataSystemComponent on // the surface data mutex. On the vegetation thread, ModifySurfacePoints in surface // components such as RiverSurfaceData can start successfully querying terrain because // the TerrainDataRequest bus is now valid, but doesn't always return fully-valid data yet. m_threadData.InterruptVegetationThread(); } void AreaSystemComponent::OnTerrainDataDestroyBegin() { // Interrupt any in-process updates until the next tick. We don't want to update // while terrain is being destroyed. There aren't any *known* race conditions here, but // there are likely surface-related race conditions, so it's better to be safe. m_threadData.InterruptVegetationThread(); } bool AreaSystemComponent::CalculateViewRect() { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); //Get the active camera. bool cameraPositionIsValid = false; AZ::Vector3 cameraPosition(0.0f); #ifdef VEGETATION_EDITOR Camera::EditorCameraRequestBus::BroadcastResult(cameraPositionIsValid, &Camera::EditorCameraRequestBus::Events::GetActiveCameraPosition, cameraPosition); if (!cameraPositionIsValid) #endif // VEGETATION_EDITOR { AZ::EntityId activeCameraId; Camera::CameraSystemRequestBus::BroadcastResult(activeCameraId, &Camera::CameraSystemRequests::GetActiveCamera); if (activeCameraId.IsValid()) { AZ::TransformBus::EventResult(cameraPosition, activeCameraId, &AZ::TransformInterface::GetWorldTranslation); cameraPositionIsValid = true; } else { //Go straight to the engine. auto engine = m_system ? m_system->GetI3DEngine() : nullptr; if (engine) { const Vec3 pos = engine->GetRenderingCamera().GetPosition(); cameraPosition = LYVec3ToAZVec3(pos); cameraPositionIsValid = true; } } } if (cameraPositionIsValid) { float posX = cameraPosition.GetX(); float posY = cameraPosition.GetY(); const int sectorSizeInMeters = m_configuration.m_sectorSizeInMeters; const int viewSize = m_configuration.m_viewRectangleSize; int halfViewSize = viewSize >> 1; posX -= halfViewSize * sectorSizeInMeters; posY -= halfViewSize * sectorSizeInMeters; auto prevViewRect = m_currViewRect; m_currViewRect.m_x = (int)(posX * m_worldToSector); m_currViewRect.m_y = (int)(posY * m_worldToSector); m_currViewRect.m_width = viewSize; m_currViewRect.m_height = viewSize; m_currViewRect.m_viewRectBounds = AZ::Aabb::CreateFromMinMax( AZ::Vector3( static_cast(m_currViewRect.m_x * sectorSizeInMeters), static_cast(m_currViewRect.m_y * sectorSizeInMeters), -AZ_FLT_MAX), AZ::Vector3( static_cast((m_currViewRect.m_x + m_currViewRect.m_width) * sectorSizeInMeters), static_cast((m_currViewRect.m_y + m_currViewRect.m_height) * sectorSizeInMeters), AZ_FLT_MAX)); return prevViewRect != m_currViewRect; } else { return false; } } AreaSystemComponent::SectorId AreaSystemComponent::GetSectorId(const AZ::Vector3& worldPos, float worldToSector) { // Convert world positions into scaled integer sector IDs. // The clamp is necessary to ensure that excessive floating-point values don't overflow // the sector range. The "nextafter" on the min/max limits are because integer min/max // lose precision when converted to float, causing them to grow to a larger range. By // using nextafter(), we push them back inside the integer range. Technically, this means // there are 128 integer numbers at each end of the range that we aren't using, but in practice // there will be many other precision bugs to deal with if we ever start using that range anyways. int wx = aznumeric_cast(AZStd::clamp(floor(static_cast(worldPos.GetX() * worldToSector)), nextafter(aznumeric_cast(std::numeric_limits::min()), 0.0f), nextafter(aznumeric_cast(std::numeric_limits::max()), 0.0f))); int wy = aznumeric_cast(AZStd::clamp(floor(static_cast(worldPos.GetY() * worldToSector)), nextafter(aznumeric_cast(std::numeric_limits::min()), 0.0f), nextafter(aznumeric_cast(std::numeric_limits::max()), 0.0f))); return SectorId(wx, wy); } AZ_FORCE_INLINE void AreaSystemComponent::ReleaseAllClaims() { // Interrupt update in process, if any m_threadData.InterruptVegetationThread(); { // Wait for vegetation update thread to finish AZStd::lock_guard lockTasks(m_threadData.m_vegetationThreadMutex); // Synchronously process any queued vegetation thread tasks on the main thread before clearing // out the sectors. This allows us to update the active vegetation area lists and mark sectors // as dirty prior to clearing them out, so that way we don't refresh them a second time after // clearing them out. // (only process if the allocation has happened) if (!(m_worldToSector <= 0.0f)) { UpdateContext threadContext; m_vegTasks.ProcessVegetationThreadTasks(&threadContext, &m_threadData); threadContext.UpdateActiveVegetationAreas(&m_threadData, m_currViewRect); } // Clear all sector data m_vegTasks.ClearSectors(); } } void AreaSystemComponent::ReleaseWithoutCleanup() { // This method will destroy all active vegetation instances, but leave the vegetation render groups active // so that we're ready to process new instances. ReleaseAllClaims(); InstanceSystemRequestBus::Broadcast(&InstanceSystemRequestBus::Events::DestroyAllInstances); } void AreaSystemComponent::ReleaseData() { // This method destroys all active vegetation instances and cleans up / unloads / destroys the vegetation render groups. ReleaseAllClaims(); InstanceSystemRequestBus::Broadcast(&InstanceSystemRequestBus::Events::Cleanup); } void AreaSystemComponent::OnCrySystemInitialized(ISystem& system, const SSystemInitParams& systemInitParams) { m_system = &system; m_system->GetISystemEventDispatcher()->RegisterListener(this); } void AreaSystemComponent::OnCrySystemShutdown(ISystem& system) { if (m_system) { m_system->GetISystemEventDispatcher()->RemoveListener(this); m_system = nullptr; } } void AreaSystemComponent::OnCryEditorBeginLevelExport() { // We need to free all our instances before exporting a level to ensure that none of the dynamic vegetation data // gets exported into the static vegetation data files. // Clear all our spawned vegetation data so that they don't get written out with the vegetation sectors. ReleaseData(); } void AreaSystemComponent::OnCryEditorEndLevelExport(bool /*success*/) { // We don't need to do anything here. When the vegetation game components reactivate themselves after the level export completes, // (see EditorVegetationComponentBase.h) they will trigger a refresh of the vegetation areas which will produce all our instances again. } void AreaSystemComponent::OnCryEditorCloseScene() { // Clear all our spawned vegetation data ReleaseData(); } void AreaSystemComponent::OnSystemEvent(ESystemEvent event, UINT_PTR wparam, UINT_PTR lparam) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); switch (event) { case ESYSTEM_EVENT_GAME_MODE_SWITCH_START: case ESYSTEM_EVENT_LEVEL_LOAD_START: case ESYSTEM_EVENT_LEVEL_UNLOAD: case ESYSTEM_EVENT_EDITOR_SIMULATION_MODE_SWITCH_START: { ReleaseData(); break; } default: break; } } ////////////////////////////////////////////////////////////////////////// // VegetationThreadTasks void AreaSystemComponent::VegetationThreadTasks::QueueVegetationTask(AZStd::function func) { AZStd::lock_guard lock(m_vegetationThreadTaskMutex); m_vegetationThreadTasks.push_back(func); if (m_debugData) { m_debugData->m_areaTaskQueueCount.store(m_vegetationThreadTasks.size(), AZStd::memory_order_relaxed); } } void AreaSystemComponent::VegetationThreadTasks::ProcessVegetationThreadTasks(UpdateContext* context, PersistentThreadData* threadData) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); VegetationThreadTasks::VegetationThreadTaskList tasks; { AZStd::lock_guard lock(m_vegetationThreadTaskMutex); AZStd::swap(tasks, m_vegetationThreadTasks); if (m_debugData) { m_debugData->m_areaTaskQueueCount.store(m_vegetationThreadTasks.size(), AZStd::memory_order_relaxed); m_debugData->m_areaTaskActiveCount.store(tasks.size(), AZStd::memory_order_relaxed); } } for (const auto& task : tasks) { task(context, threadData, this); if (m_debugData) { m_debugData->m_areaTaskActiveCount.fetch_sub(1, AZStd::memory_order_relaxed); } } } AZ::Aabb AreaSystemComponent::VegetationThreadTasks::GetSectorBounds(const SectorId& sectorId, int sectorSizeInMeters) { return AZ::Aabb::CreateFromMinMax( AZ::Vector3( static_cast(sectorId.first * sectorSizeInMeters), static_cast(sectorId.second * sectorSizeInMeters), -AZ_FLT_MAX), AZ::Vector3( static_cast((sectorId.first + 1) * sectorSizeInMeters), static_cast((sectorId.second + 1) * sectorSizeInMeters), AZ_FLT_MAX)); } const AreaSystemComponent::SectorInfo* AreaSystemComponent::VegetationThreadTasks::GetSector(const SectorId& sectorId) const { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); AZStd::lock_guard lock(m_sectorRollingWindowMutex); auto itSector = m_sectorRollingWindow.find(sectorId); return itSector != m_sectorRollingWindow.end() ? &itSector->second : nullptr; } AreaSystemComponent::SectorInfo* AreaSystemComponent::VegetationThreadTasks::GetSector(const SectorId& sectorId) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); AZStd::lock_guard lock(m_sectorRollingWindowMutex); auto itSector = m_sectorRollingWindow.find(sectorId); return itSector != m_sectorRollingWindow.end() ? &itSector->second : nullptr; } AreaSystemComponent::SectorInfo* AreaSystemComponent::VegetationThreadTasks::CreateSector(const SectorId& sectorId, int sectorDensity, int sectorSizeInMeters, SnapMode sectorPointSnapMode) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); SectorInfo sectorInfo; sectorInfo.m_id = sectorId; sectorInfo.m_bounds = GetSectorBounds(sectorId, sectorSizeInMeters); UpdateSectorPoints(sectorInfo, sectorDensity, sectorSizeInMeters, sectorPointSnapMode); AZStd::lock_guard lock(m_sectorRollingWindowMutex); SectorInfo& sectorInfoRef = m_sectorRollingWindow[sectorInfo.m_id] = AZStd::move(sectorInfo); UpdateSectorCallbacks(sectorInfoRef); return §orInfoRef; } void AreaSystemComponent::VegetationThreadTasks::UpdateSectorPoints(SectorInfo& sectorInfo, int sectorDensity, int sectorSizeInMeters, SnapMode sectorPointSnapMode) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); const float vegStep = sectorSizeInMeters / static_cast(sectorDensity); //build a free list of all points in the sector for areas to consume sectorInfo.m_baseContext.m_masks.clear(); sectorInfo.m_baseContext.m_availablePoints.clear(); sectorInfo.m_baseContext.m_availablePoints.reserve(sectorDensity * sectorDensity); // Determine within our texel area where we want to create our vegetation positions: // 0 = lower left corner, 0.5 = center const float texelOffset = (sectorPointSnapMode == SnapMode::Center) ? 0.5f : 0.0f; SurfaceData::SurfacePointListPerPosition availablePointsPerPosition; AZ::Vector2 stepSize(vegStep, vegStep); AZ::Vector3 regionOffset(texelOffset * vegStep, texelOffset * vegStep, 0.0f); AZ::Aabb regionBounds = sectorInfo.m_bounds; regionBounds.SetMin(regionBounds.GetMin() + regionOffset); // If we just used the sector bounds, floating-point error could sometimes cause an extra point to get generated // right at the max edge of the bounds. So instead, we adjust our max placement bounds to be the exact size needed // for sectorDensity points to get placed, plus half a vegStep to account for a safe margin of floating-point error. // The exact size would be (sectorDensity - 1), so adding half a vegStep gives us (sectorDensity - 0.5f). // (We should be able to add anything less than 1 extra vegStep and still get exactly sectorDensity points) regionBounds.SetMax(regionBounds.GetMin() + AZ::Vector3(vegStep * (sectorDensity - 0.5f), vegStep * (sectorDensity - 0.5f), 0.0f)); SurfaceData::SurfaceDataSystemRequestBus::Broadcast( &SurfaceData::SurfaceDataSystemRequestBus::Events::GetSurfacePointsFromRegion, regionBounds, stepSize, SurfaceData::SurfaceTagVector(), availablePointsPerPosition); AZ_Assert(availablePointsPerPosition.size() == (sectorDensity * sectorDensity), "Veg sector ended up with unexpected density (%d points created, %d expected)", availablePointsPerPosition.size(), (sectorDensity * sectorDensity)); uint claimIndex = 0; for (auto& availablePoints : availablePointsPerPosition) { for (auto& surfacePoint : availablePoints.second) { sectorInfo.m_baseContext.m_availablePoints.push_back(); ClaimPoint& claimPoint = sectorInfo.m_baseContext.m_availablePoints.back(); claimPoint.m_handle = CreateClaimHandle(sectorInfo, ++claimIndex); claimPoint.m_position = surfacePoint.m_position; claimPoint.m_normal = surfacePoint.m_normal; claimPoint.m_masks = surfacePoint.m_masks; SurfaceData::AddMaxValueForMasks(sectorInfo.m_baseContext.m_masks, surfacePoint.m_masks); } } } void AreaSystemComponent::VegetationThreadTasks::UpdateSectorCallbacks(SectorInfo& sectorInfo) { //setup callback to test if matching point is already claimed sectorInfo.m_baseContext.m_existedCallback = [this, §orInfo](const ClaimPoint& point, const InstanceData& instanceData) { const ClaimHandle handle = point.m_handle; auto claimItr = sectorInfo.m_claimedWorldPointsBeforeFill.find(handle); bool exists = (claimItr != sectorInfo.m_claimedWorldPointsBeforeFill.end()) && InstanceData::IsSameInstanceData(instanceData, claimItr->second); if (exists) { CreateClaim(sectorInfo, handle, instanceData); VEG_PROFILE_METHOD(DebugNotificationBus::TryQueueBroadcast(&DebugNotificationBus::Events::CreateInstance, instanceData.m_instanceId, instanceData.m_position, instanceData.m_id)); } return exists; }; //setup callback to create claims for new instances sectorInfo.m_baseContext.m_createdCallback = [this, §orInfo](const ClaimPoint& point, const InstanceData& instanceData) { const ClaimHandle handle = point.m_handle; auto claimItr = sectorInfo.m_claimedWorldPointsBeforeFill.find(handle); bool exists = claimItr != sectorInfo.m_claimedWorldPointsBeforeFill.end(); if (exists) { const auto& claimedInstanceData = claimItr->second; if (claimedInstanceData.m_id != instanceData.m_id) { //must force bus connect if areas are different AreaNotificationBus::Event(claimedInstanceData.m_id, &AreaNotificationBus::Events::OnAreaConnect); AreaRequestBus::Event(claimedInstanceData.m_id, &AreaRequestBus::Events::UnclaimPosition, handle); AreaNotificationBus::Event(claimedInstanceData.m_id, &AreaNotificationBus::Events::OnAreaDisconnect); } else { //already connected during fill sector AreaRequestBus::Event(claimedInstanceData.m_id, &AreaRequestBus::Events::UnclaimPosition, handle); } } CreateClaim(sectorInfo, handle, instanceData); }; } void AreaSystemComponent::VegetationThreadTasks::DeleteSector(const SectorId& sectorId) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); AZStd::lock_guard lock(m_sectorRollingWindowMutex); auto itSector = m_sectorRollingWindow.find(sectorId); if (itSector != m_sectorRollingWindow.end()) { SectorInfo& sectorInfo(itSector->second); EmptySector(sectorInfo); m_sectorRollingWindow.erase(itSector); } else { AZ_Assert(false, "Sector marked for deletion but doesn't exist"); } } template inline void AreaSystemComponent::VegetationThreadTasks::EnumerateSectorsInAabb(const AZ::Aabb& bounds, float worldToSector, const ViewRect& viewRect, Fn&& fn) { // Get the min/max sectors for the AABB. If an invalid AABB is passed in, process every active sector. // (i.e. every sector in the current m_currViewRect) const SectorId boundsMinSector = bounds.IsValid() ? GetSectorId(bounds.GetMin(), worldToSector) : viewRect.GetMinSector(); const SectorId boundsMaxSector = bounds.IsValid() ? GetSectorId(bounds.GetMax(), worldToSector) : viewRect.GetMaxSector(); // The min bounds are set to the larger of the AABB min and the curr view rect min. // The max bounds are set to the smaller of the AABB max and the curr view rect max. // This lets us process only the sectors that overlap both. // Note that if the AABB doesn't overlap the curr view rect, the max will end up less // than the min, in which case we process no sectors. const int minX = AZStd::GetMax(boundsMinSector.first, viewRect.GetMinXSector()); const int minY = AZStd::GetMax(boundsMinSector.second, viewRect.GetMinYSector()); const int maxX = AZStd::GetMin(boundsMaxSector.first, viewRect.GetMaxXSector()); const int maxY = AZStd::GetMin(boundsMaxSector.second, viewRect.GetMaxYSector()); for (int currY = minY; currY <= maxY; ++currY) { for (int currX = minX; currX <= maxX; ++currX) { if (!fn(AZStd::move(SectorId(currX, currY)))) { return; } } } } void AreaSystemComponent::VegetationThreadTasks::AddUnregisteredVegetationArea(const VegetationAreaInfo& area, float worldToSector, const ViewRect& viewRect) { EnumerateSectorsInAabb(area.m_bounds, worldToSector, viewRect, [&](SectorId&& sectorId) { m_unregisteredVegetationAreaSet[AZStd::move(sectorId)].insert(area.m_id); return true; }); } void AreaSystemComponent::VegetationThreadTasks::ReleaseUnregisteredClaims(SectorInfo& sectorInfo) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); if (!m_unregisteredVegetationAreaSet.empty()) { auto unregisteredAreasForSector = m_unregisteredVegetationAreaSet.find(sectorInfo.m_id); if (unregisteredAreasForSector != m_unregisteredVegetationAreaSet.end()) { for (auto claimItr = sectorInfo.m_claimedWorldPoints.begin(); claimItr != sectorInfo.m_claimedWorldPoints.end(); ) { if (unregisteredAreasForSector->second.find(claimItr->second.m_id) != unregisteredAreasForSector->second.end()) { claimItr = sectorInfo.m_claimedWorldPoints.erase(claimItr); } else { ++claimItr; } } m_unregisteredVegetationAreaSet.erase(unregisteredAreasForSector); } } } void AreaSystemComponent::VegetationThreadTasks::ReleaseUnusedClaims(SectorInfo& sectorInfo) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); AZStd::unordered_map> claimsToRelease; // Group up all the previously-claimed-but-no-longer-claimed points based on area id for (const auto& claimPair : sectorInfo.m_claimedWorldPointsBeforeFill) { const auto& handle = claimPair.first; const auto& instanceData = claimPair.second; const auto& areaId = instanceData.m_id; if (sectorInfo.m_claimedWorldPoints.find(handle) == sectorInfo.m_claimedWorldPoints.end()) { claimsToRelease[areaId].insert(handle); } } sectorInfo.m_claimedWorldPointsBeforeFill.clear(); // Iterate over the claims by area id and release them for (const auto& claimPair : claimsToRelease) { const auto& areaId = claimPair.first; const auto& handles = claimPair.second; AreaNotificationBus::Event(areaId, &AreaNotificationBus::Events::OnAreaConnect); for (const auto& handle : handles) { AreaRequestBus::Event(areaId, &AreaRequestBus::Events::UnclaimPosition, handle); } AreaNotificationBus::Event(areaId, &AreaNotificationBus::Events::OnAreaDisconnect); } } void AreaSystemComponent::VegetationThreadTasks::FillSector(SectorInfo& sectorInfo, const VegetationAreaVector& activeAreas) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); VEG_PROFILE_METHOD(DebugNotificationBus::TryQueueBroadcast(&DebugNotificationBus::Events::FillSectorStart, sectorInfo.GetSectorX(), sectorInfo.GetSectorY(), AZStd::chrono::system_clock::now())); ReleaseUnregisteredClaims(sectorInfo); //m_availablePoints is a free list initialized with the complete set of points in the sector. ClaimContext activeContext = sectorInfo.m_baseContext; // Clear out the list of claimed world points before we begin sectorInfo.m_claimedWorldPointsBeforeFill = sectorInfo.m_claimedWorldPoints; sectorInfo.m_claimedWorldPoints.clear(); //for all active areas attempt to spawn vegetation on sector grid positions for (const auto& area : activeAreas) { //if one or more areas claimed all the points in m_availablePoints, there's no reason to continue. if (activeContext.m_availablePoints.empty()) { break; } //only consider areas that intersect this sector if (!area.m_bounds.IsValid() || area.m_bounds.Overlaps(sectorInfo.m_bounds)) { VEG_PROFILE_METHOD(DebugNotificationBus::TryQueueBroadcast(&DebugNotificationBus::Events::FillAreaStart, area.m_id, AZStd::chrono::system_clock::now())); //each area is responsible for removing whatever points it claims from m_availablePoints, so subsequent areas will have fewer points to try to claim. AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaConnect); AreaRequestBus::Event(area.m_id, &AreaRequestBus::Events::ClaimPositions, EntityIdStack{}, activeContext); AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaDisconnect); VEG_PROFILE_METHOD(DebugNotificationBus::TryQueueBroadcast(&DebugNotificationBus::Events::FillAreaEnd, area.m_id, AZStd::chrono::system_clock::now(), aznumeric_cast(activeContext.m_availablePoints.size()))); } } size_t remainingPointCount = activeContext.m_availablePoints.size(); ReleaseUnusedClaims(sectorInfo); VEG_PROFILE_METHOD(DebugNotificationBus::TryQueueBroadcast(&DebugNotificationBus::Events::FillSectorEnd, sectorInfo.GetSectorX(), sectorInfo.GetSectorY(), AZStd::chrono::system_clock::now(), aznumeric_cast(remainingPointCount))); } void AreaSystemComponent::VegetationThreadTasks::EmptySector(SectorInfo& sectorInfo) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); AZStd::unordered_map> claimsToRelease; // group up all the points based on area id for (const auto& claimPair : sectorInfo.m_claimedWorldPoints) { const auto& handle = claimPair.first; const auto& instanceData = claimPair.second; const auto& areaId = instanceData.m_id; claimsToRelease[areaId].insert(handle); } sectorInfo.m_claimedWorldPoints.clear(); // iterate over the claims by area id and release them for (const auto& claimPair : claimsToRelease) { const auto& areaId = claimPair.first; const auto& handles = claimPair.second; AreaNotificationBus::Event(areaId, &AreaNotificationBus::Events::OnAreaConnect); for (const auto& handle : handles) { AreaRequestBus::Event(areaId, &AreaRequestBus::Events::UnclaimPosition, handle); } AreaNotificationBus::Event(areaId, &AreaNotificationBus::Events::OnAreaDisconnect); } } void AreaSystemComponent::VegetationThreadTasks::ClearSectors() { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); AZStd::lock_guard lock(m_sectorRollingWindowMutex); for (auto& sectorPair : m_sectorRollingWindow) { EmptySector(sectorPair.second); } m_sectorRollingWindow.clear(); // Clear any pending unregistrations; since all of the sectors have been cleared anyways, these don't affect anything m_unregisteredVegetationAreaSet.clear(); } void AreaSystemComponent::VegetationThreadTasks::CreateClaim(SectorInfo& sectorInfo, const ClaimHandle handle, const InstanceData& instanceData) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); sectorInfo.m_claimedWorldPoints[handle] = instanceData; } ClaimHandle AreaSystemComponent::VegetationThreadTasks::CreateClaimHandle(const SectorInfo& sectorInfo, uint32_t index) const { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); ClaimHandle handle = 0; AreaSystemUtil::hash_combine_64(handle, sectorInfo.m_id.first); AreaSystemUtil::hash_combine_64(handle, sectorInfo.m_id.second); AreaSystemUtil::hash_combine_64(handle, index); return handle; } void AreaSystemComponent::VegetationThreadTasks::MarkDirtySectors(const AZ::Aabb& bounds, DirtySectors& dirtySet, float worldToSector, const ViewRect& viewRect) { if (bounds.IsValid()) { if (!dirtySet.IsAllDirty()) { // Only mark individual sectors as dirty if we have valid AABB bounds and haven't // already marked *all* sectors as dirty. EnumerateSectorsInAabb(bounds, worldToSector, viewRect, [&](SectorId&& sectorId) { dirtySet.MarkDirty(sectorId); return true; }); } } else { // If we have invalid bounds, we can mark all sectors as dirty without needing // to add each one to the list. dirtySet.MarkAllDirty(); } } void AreaSystemComponent::VegetationThreadTasks::FetchDebugData() { VEG_PROFILE_METHOD(DebugSystemDataBus::BroadcastResult(m_debugData, &DebugSystemDataBus::Events::GetDebugData)); } ////////////////////////////////////////////////////////////////////////// // PersistentThreadData AZ_FORCE_INLINE void AreaSystemComponent::PersistentThreadData::InterruptVegetationThread() { auto expected = PersistentThreadData::VegetationThreadState::Running; m_vegetationThreadState.compare_exchange_strong(expected, PersistentThreadData::VegetationThreadState::InterruptRequested); } ////////////////////////////////////////////////////////////////////////// // UpdateContext void AreaSystemComponent::UpdateContext::Run(PersistentThreadData* threadData, VegetationThreadTasks* vegTasks, CachedMainThreadData* cachedMainThreadData) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); // Ensure that the main thread doesn't activate or deactivate the component until after this thread finishes. // Note that this does *not* prevent the main thread from running OnTick, which can communicate data changes // to this thread while it's still processing work. AZStd::lock_guardm_vegetationThreadMutex)> lockTasks(threadData->m_vegetationThreadMutex); bool keepProcessing = true; while (keepProcessing && (threadData->m_vegetationThreadState != PersistentThreadData::VegetationThreadState::InterruptRequested)) { AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::Entity, "Vegetation::AreaSystemComponent::UpdateContext::Run-InnerLoop"); // Update thread state if its dirty PersistentThreadData::VegetationDataSyncState expected = PersistentThreadData::VegetationDataSyncState::Dirty; if (threadData->m_vegetationDataSyncState.compare_exchange_strong(expected, PersistentThreadData::VegetationDataSyncState::Updating)) { // A dirty state can consist of one or more of the following: // - Main thread has changed veg configuration // - Main thread has changed current view rectangle // - Vegetation tasks have been queued for this thread to process // Our main thread has potentially updated its state, so cache a new copy of the pieces of state we need. m_cachedMainThreadData = *cachedMainThreadData; // Run through all the queued tasks to update vegetation area active states and lists of dirty sectors vegTasks->ProcessVegetationThreadTasks(this, threadData); // Now that we've processed all the queued tasks, gather a list of active areas that affect our visible sectors, sorted by priority UpdateActiveVegetationAreas(threadData, m_cachedMainThreadData.m_currViewRect); // Refresh the lists of sectors to create / update / remove keepProcessing = UpdateSectorWorkLists(threadData, vegTasks); // We've finished refreshing the thread work state, so mark ourselves as synchronized. threadData->m_vegetationDataSyncState = PersistentThreadData::VegetationDataSyncState::Synchronized; } if (keepProcessing) { keepProcessing = UpdateOneSector(threadData, vegTasks); } } } void AreaSystemComponent::UpdateContext::UpdateActiveVegetationAreas(PersistentThreadData* threadData, const ViewRect& viewRect) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); //build a priority sorted list of all active areas if (threadData->m_activeAreasDirty) { threadData->m_activeAreasDirty = false; threadData->m_activeAreas.clear(); threadData->m_activeAreas.reserve(threadData->m_globalVegetationAreaMap.size()); for (const auto& areaPair : threadData->m_globalVegetationAreaMap) { const auto& area = areaPair.second; //if this is an area being ignored due to a parent area blender, skip it if (threadData->m_ignoredVegetationAreaSet.find(area.m_id) != threadData->m_ignoredVegetationAreaSet.end()) { continue; } //do any per area setup or checks since the state of areas and entities with the system has changed bool prepared = false; AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaConnect); AreaRequestBus::EventResult(prepared, area.m_id, &AreaRequestBus::Events::PrepareToClaim, EntityIdStack{}); AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaDisconnect); if (!prepared) { // if PrepareToClaim returned false, this area is declaring itself as inactive. // The area will need to call RefreshArea() if/when its state should change to active. continue; } threadData->m_activeAreas.push_back(area); } AZStd::sort(threadData->m_activeAreas.begin(), threadData->m_activeAreas.end(), [](const auto& lhs, const auto& rhs) { return AZStd::make_pair(lhs.m_layer, lhs.m_priority) > AZStd::make_pair(rhs.m_layer, rhs.m_priority); }); } //further reduce set of active areas to only include ones that intersect the bubble AZ::Aabb bubbleBounds = viewRect.GetViewRectBounds(); threadData->m_activeAreasInBubble = threadData->m_activeAreas; threadData->m_activeAreasInBubble.erase( AZStd::remove_if( threadData->m_activeAreasInBubble.begin(), threadData->m_activeAreasInBubble.end(), [bubbleBounds](const auto& area) { return area.m_bounds.IsValid() && !area.m_bounds.Overlaps(bubbleBounds); }), threadData->m_activeAreasInBubble.end()); } bool AreaSystemComponent::UpdateContext::UpdateSectorWorkLists(PersistentThreadData* threadData, VegetationThreadTasks* vegTasks) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); auto& worldToSector = m_cachedMainThreadData.m_worldToSector; auto& currViewRect = m_cachedMainThreadData.m_currViewRect; auto& sectorSizeInMeters = m_cachedMainThreadData.m_sectorSizeInMeters; // only process the sectors if the allocation has happened if (worldToSector <= 0.0f) { return false; } bool deleteAllSectors = false; // Early exit if no active areas, no sectors are marked as dirty or updating, and there // are no sectors left in our rolling window. // Until an area becomes active again, there's no work that sectors should need to do. if (threadData->m_activeAreasInBubble.empty() && threadData->m_dirtySectorContents.IsNoneDirty() && threadData->m_dirtySectorSurfacePoints.IsNoneDirty()) { AZStd::lock_guardm_sectorRollingWindowMutex)> lock(vegTasks->m_sectorRollingWindowMutex); if (vegTasks->m_sectorRollingWindow.empty()) { return !m_deleteWorkList.empty() || !m_updateWorkList.empty(); } else { // No active areas left in our view bubble, so queue up the deletion of all remaining active sectors. deleteAllSectors = true; } } // Cache off the total number of sectors that *should* be active in the view rectangle. We'll use this // when processing sectors to ensure that we start to prioritize deletes whenever our number of active sectors // gets above this number. m_viewRectSectorCount = currViewRect.GetNumSectors(); // remove any sectors marked for update which are no longer in the view rectangle m_updateWorkList.erase( AZStd::remove_if( m_updateWorkList.begin(), m_updateWorkList.end(), [currViewRect](const auto& entry) {return !currViewRect.IsInside(entry.first); }), m_updateWorkList.end()); AZ_Assert(m_updateWorkList.size() <= m_viewRectSectorCount, "Refreshed RequestedUpdate list should not be larger than the view rectangle."); // Clear our delete work list, we'll recreate it and sort it again below. // Note: We do NOT clear m_updateWorkList, because we use it to incrementally determine any new // updates to add to the queue. Without it, we wouldn't know if a previous data change caused // us to mark any sectors still in view as needing an update. m_deleteWorkList.clear(); // If we're deleting all sectors, make sure we don't have any of them previously queued up for creation / updating. if (deleteAllSectors) { m_updateWorkList.clear(); } // Run through our list of active sectors and determine which ones need adding / updating / deleting { AZStd::lock_guardm_sectorRollingWindowMutex)> lock(vegTasks->m_sectorRollingWindowMutex); // To create our add / update / delete lists, we need two loops. The first loops through the *new* view rectangle // looking for missing sectors to add. The second loops through the *current* set of active sectors looking for any // to update or remove. // First loop: Determine non-existent sectors which need to be created if (!deleteAllSectors) { for (int y = currViewRect.m_y; y < currViewRect.m_y + currViewRect.m_height; ++y) { for (int x = currViewRect.m_x; x < currViewRect.m_x + currViewRect.m_width; ++x) { SectorId sectorId(x, y); if (vegTasks->m_sectorRollingWindow.find(sectorId) == vegTasks->m_sectorRollingWindow.end()) { // If the sector doesn't currently exist and it belongs in the view rect, request a creation. // (This will either create a new entry or overwrite an existing pending Create request) auto found = AZStd::find_if(m_updateWorkList.begin(), m_updateWorkList.end(), [sectorId](auto& entry) { return (entry.first == sectorId); }); if (found != m_updateWorkList.end()) { // If the update entry already exists, overwrite the state. We don't need to check or // preserve the existing state because Create is the most comprehensive update we can do. found->second = UpdateMode::Create; } else { m_updateWorkList.emplace_back(sectorId, UpdateMode::Create); } // Since we've already removed entries that aren't in the view rect, and these loops are only // adding entries in the view rect, at this point our update work list size should never get // larger than the set of sectors in the view rect. AZ_Assert(m_updateWorkList.size() <= m_viewRectSectorCount, "Too many update requests added"); } } } } // Second loop: Determine any existing sectors which need to be updated or deleted for (auto& sector : vegTasks->m_sectorRollingWindow) { auto& sectorId = sector.first; if (deleteAllSectors || !currViewRect.IsInside(sectorId)) { // Active sector is no longer within view or there are no active areas, so delete it m_deleteWorkList.emplace_back(AZStd::move(sectorId)); } else if (threadData->m_dirtySectorSurfacePoints.IsDirty(sectorId)) { // Active sector has new surface point information, so rebuild surface cache and fill // (This will either create a new entry, or overwrite an existing fill or rebuild request) auto found = AZStd::find_if(m_updateWorkList.begin(), m_updateWorkList.end(), [sectorId](auto& entry) { return (entry.first == sectorId); }); if (found != m_updateWorkList.end()) { // If the update entry already exists, overwrite the state. We don't need to check or // preserve the state since it should only contain either Rebuild or Fill, and Rebuild // is more comprehensive than Fill. AZ_Assert(found->second != UpdateMode::Create, "Create requests shouldn't exist for active sectors!"); found->second = UpdateMode::RebuildSurfaceCacheAndFill; } else { m_updateWorkList.emplace_back(sectorId, UpdateMode::RebuildSurfaceCacheAndFill); } // We shouldn't ever have an update list that's larger than the set of sectors in the view rect. AZ_Assert(m_updateWorkList.size() <= m_viewRectSectorCount, "Too many update requests added"); } else if (threadData->m_dirtySectorContents.IsDirty(sectorId)) { // Active sector has new veg area information, so refill it. auto found = AZStd::find_if(m_updateWorkList.begin(), m_updateWorkList.end(), [sectorId](auto& entry) { return (entry.first == sectorId); }); if (found == m_updateWorkList.end()) { // Only add Fill entries if no update request exists for this sector. We don't // overwrite existing entries because an existing entry might have previously // requested "RebuildSurfaceCacheAndFill", which is more comprehensive than this request. m_updateWorkList.emplace_back(sectorId, UpdateMode::Fill); // We shouldn't ever have an update list that's larger than the set of sectors in the view rect. AZ_Assert(m_updateWorkList.size() <= m_viewRectSectorCount, "Too many update requests added"); } } } } // We've finished processing our dirtySector lists, so clear them. threadData->m_dirtySectorContents.Clear(); threadData->m_dirtySectorSurfacePoints.Clear(); // sort work by distance from center of the view rectangle. if (currViewRect.GetViewRectBounds().IsValid()) { float sectorCenterX = (currViewRect.GetMinXSector() + currViewRect.GetMaxXSector()) / 2.0f; float sectorCenterY = (currViewRect.GetMinYSector() + currViewRect.GetMaxYSector()) / 2.0f; // Sort function that returns true if the lhs is "closer" than the rhs to the center. // The choice of sort algorithm is somewhat a question of preference, and could potentially be made a policy // choice at some point. The current choice uses "number of sectors from center" as the primary sort criteria, // with a secondary sort on y and x values to get a deterministic sort pattern. This algorithm updates the vegetation // outward in cocentric circles. // Here are some other possibilities of algorithm choices: // 1) float maxDist = AZStd::GetMax(fabs(id.first - sectorCenterX), fabs(id.second - sectorCenterY)); // This moves outward in cocentric squares. // 2) float maxDist = GetSectorBounds(id).GetCenter().GetDistanceSq(currViewRect.GetViewRectBounds().GetCenter()); // This moves outward in cocentric circles, similar to our chosen algorithm, but in more of a "pinwheel" pattern // that fans out from the axis lines. // 3) We could feed in camera orientation as well, and use that to further prioritize sectors within view. The concern // with choosing this approach is that it will update the work lists much more rapidly than the vegetation can spawn, so // it the extra updates and calculations could easily cause sector choices that constantly lag behind the current view, // producing similar to worse results than our current algorithm. // With any of these choices, the secondary sort gives a deterministic update pattern when the distances are equal. auto sectorCloserToCenter = [sectorCenterX, sectorCenterY](const SectorId& lhs, const SectorId& rhs) { float lhsMaxDist = ((lhs.first - sectorCenterX) * (lhs.first - sectorCenterX)) + ((lhs.second - sectorCenterY) * (lhs.second - sectorCenterY)); float rhsMaxDist = ((rhs.first - sectorCenterX) * (rhs.first - sectorCenterX)) + ((rhs.second - sectorCenterY) * (rhs.second - sectorCenterY)); return (lhsMaxDist < rhsMaxDist) || // return if one sector is closer than the other to the center ((lhsMaxDist == rhsMaxDist) && // if it's the same distance... ((lhs.second < rhs.second) || // return if the sector Y value is smaller ((lhs.second == rhs.second) && (lhs.first < rhs.first)))); // if the Y value is the same, return if the sector X value is smaller }; AZStd::sort(m_updateWorkList.begin(), m_updateWorkList.end(), [sectorCloserToCenter](const auto& lhs, const auto& rhs) { // We always pull from the end of the list, so we sort the *closest* sectors to the end. // That way we create / update the closest sectors first. return !sectorCloserToCenter(lhs.first, rhs.first); }); AZStd::sort(m_deleteWorkList.begin(), m_deleteWorkList.end(), [sectorCloserToCenter](const auto& lhs, const auto& rhs) { // We always pull from the end of the list, so we sort the *furthest* sectors to the end. // That way we delete the furthest sectors first. return sectorCloserToCenter(lhs, rhs); }); } return !m_deleteWorkList.empty() || !m_updateWorkList.empty(); } bool AreaSystemComponent::UpdateContext::UpdateOneSector(PersistentThreadData* threadData, VegetationThreadTasks* vegTasks) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Entity); // This chooses work in the following order: // 1) Delete if we have more sectors than the total that should be in the view rectangle // 2) Create/update if we have any sectors to create / update // 3) Delete if we have any sectors to delete // Delete if there are more active sectors than the number of desired sectors or the update list is empty. if (!m_deleteWorkList.empty()) { AZStd::lock_guardm_sectorRollingWindowMutex)> lock(vegTasks->m_sectorRollingWindowMutex); if ((vegTasks->m_sectorRollingWindow.size() > m_viewRectSectorCount) || m_updateWorkList.empty()) { vegTasks->DeleteSector(m_deleteWorkList.back()); m_deleteWorkList.pop_back(); return true; } } // Create / update if there's anything to do and we didn't prioritize a delete. if (!m_updateWorkList.empty()) { auto& updateEntry = m_updateWorkList.back(); SectorId sectorId = updateEntry.first; UpdateMode mode = updateEntry.second; m_updateWorkList.pop_back(); { AZStd::lock_guardm_sectorRollingWindowMutex)> lock(vegTasks->m_sectorRollingWindowMutex); auto& sectorDensity = m_cachedMainThreadData.m_sectorDensity; auto& sectorSizeInMeters = m_cachedMainThreadData.m_sectorSizeInMeters; auto& sectorPointSnapMode = m_cachedMainThreadData.m_sectorPointSnapMode; switch (mode) { case UpdateMode::RebuildSurfaceCacheAndFill: { auto sectorInfo = vegTasks->GetSector(sectorId); AZ_Assert(sectorInfo, "Sector update mode is 'RebuildSurfaceCache' but sector doesn't exist"); vegTasks->UpdateSectorPoints(*sectorInfo, sectorDensity, sectorSizeInMeters, sectorPointSnapMode); vegTasks->FillSector(*sectorInfo, threadData->m_activeAreasInBubble); } break; case UpdateMode::Fill: { auto sectorInfo = vegTasks->GetSector(sectorId); AZ_Assert(sectorInfo, "Sector update mode is 'Fill' but sector doesn't exist"); vegTasks->FillSector(*sectorInfo, threadData->m_activeAreasInBubble); } break; case UpdateMode::Create: { AZ_Assert(!vegTasks->GetSector(sectorId), "Sector update mode is 'Create' but sector already exists"); auto sectorInfo = vegTasks->CreateSector(sectorId, sectorDensity, sectorSizeInMeters, sectorPointSnapMode); vegTasks->FillSector(*sectorInfo, threadData->m_activeAreasInBubble); } break; } } return true; } // No sectors left to process, so tell our main loop to stop processing. return false; } }