From a0a9314fed3c496ab8510936466105dc03d966ee Mon Sep 17 00:00:00 2001 From: Mikhail Agapov <118179774+mikhail-dcl@users.noreply.github.com> Date: Tue, 2 Jan 2024 12:37:44 +0100 Subject: [PATCH] Jobify scenes sorting (#239) --- .../Prioritization/DistanceBasedComparer.cs | 20 +++++- ...solveSceneStateByIncreasingRadiusSystem.cs | 68 ++++++++++++++----- ...ceneStateByIncreasingRadiusSystemShould.cs | 12 +++- .../Utility/ParcelMathJobifiedHelper.cs | 13 +++- 4 files changed, 92 insertions(+), 21 deletions(-) diff --git a/Explorer/Assets/Scripts/ECS/Prioritization/DistanceBasedComparer.cs b/Explorer/Assets/Scripts/ECS/Prioritization/DistanceBasedComparer.cs index 5e595101d0..79c2b7e691 100644 --- a/Explorer/Assets/Scripts/ECS/Prioritization/DistanceBasedComparer.cs +++ b/Explorer/Assets/Scripts/ECS/Prioritization/DistanceBasedComparer.cs @@ -8,7 +8,10 @@ public class DistanceBasedComparer : IComparer { public static readonly DistanceBasedComparer INSTANCE = new (); - public int Compare(IPartitionComponent x, IPartitionComponent y) + public int Compare(IPartitionComponent x, IPartitionComponent y) => + Compare(new DataSurrogate(x.RawSqrDistance, x.IsBehind), new DataSurrogate(y.RawSqrDistance, y.IsBehind)); + + public static int Compare(DataSurrogate x, DataSurrogate y) { // discrete distance comparison // break down by SQR_PARCEL_SIZE @@ -19,5 +22,20 @@ public int Compare(IPartitionComponent x, IPartitionComponent y) int bucketComparison = xParcelBucket.CompareTo(yParcelBucket); return bucketComparison != 0 ? bucketComparison : x.IsBehind.CompareTo(y.IsBehind); } + + /// + /// Blittable data to be used in the comparer + /// + public readonly struct DataSurrogate + { + public readonly bool IsBehind; + public readonly float RawSqrDistance; + + public DataSurrogate(float rawSqrDistance, bool isBehind) + { + RawSqrDistance = rawSqrDistance; + IsBehind = isBehind; + } + } } } diff --git a/Explorer/Assets/Scripts/ECS/SceneLifeCycle/IncreasingRadius/ResolveSceneStateByIncreasingRadiusSystem.cs b/Explorer/Assets/Scripts/ECS/SceneLifeCycle/IncreasingRadius/ResolveSceneStateByIncreasingRadiusSystem.cs index 453e7e5f9b..d5488cc2e7 100644 --- a/Explorer/Assets/Scripts/ECS/SceneLifeCycle/IncreasingRadius/ResolveSceneStateByIncreasingRadiusSystem.cs +++ b/Explorer/Assets/Scripts/ECS/SceneLifeCycle/IncreasingRadius/ResolveSceneStateByIncreasingRadiusSystem.cs @@ -14,8 +14,9 @@ using SceneRunner.Scene; using System.Collections.Generic; using System.Runtime.CompilerServices; +using Unity.Collections; +using Unity.Jobs; using UnityEngine; -using UnityEngine.Pool; using Utility; namespace ECS.SceneLifeCycle.IncreasingRadius @@ -31,24 +32,31 @@ namespace ECS.SceneLifeCycle.IncreasingRadius [UpdateAfter(typeof(CreateEmptyPointersInFixedRealmSystem))] public partial class ResolveSceneStateByIncreasingRadiusSystem : BaseUnityLoopSystem { + private static readonly Comparer COMPARER_INSTANCE = new (); + private static readonly QueryDescription START_SCENES_LOADING = new QueryDescription() .WithAll() .WithNone>(); private readonly IRealmPartitionSettings realmPartitionSettings; - private readonly List orderedData; + internal JobHandle? sortingJobHandle; + + private NativeList orderedData; internal ResolveSceneStateByIncreasingRadiusSystem(World world, IRealmPartitionSettings realmPartitionSettings) : base(world) { this.realmPartitionSettings = realmPartitionSettings; - orderedData = ListPool.Get(); + // Set initial capacity to 1/3 of the total capacity required for all rings + orderedData = new NativeList( + ParcelMathJobifiedHelper.GetRingsArraySize(realmPartitionSettings.MaxLoadingDistanceInParcels) / 3, + Allocator.Persistent); } public override void Dispose() { - ListPool.Release(orderedData); + orderedData.Dispose(); } protected override void Update(float t) @@ -103,6 +111,15 @@ private void ProcessesFixedRealm([Data] float maxLoadingSqrDistance, ref RealmCo private void StartScenesLoading(ref RealmComponent realmComponent, float maxLoadingSqrDistance) { + if (sortingJobHandle is { IsCompleted: true }) + { + sortingJobHandle.Value.Complete(); + CreatePromisesFromOrderedData(realmComponent.Ipfs); + } + + if (sortingJobHandle is { IsCompleted: false }) return; + + // Start new sorting // Order the scenes definitions by the CURRENT partition and serve first N of them orderedData.Clear(); @@ -110,12 +127,10 @@ private void StartScenesLoading(ref RealmComponent realmComponent, float maxLoad foreach (ref Chunk chunk in World.Query(in START_SCENES_LOADING)) { ref Entity entityFirstElement = ref chunk.Entity(0); - ref SceneDefinitionComponent sceneDefinitionFirst = ref chunk.GetFirst(); ref PartitionComponent partitionComponentFirst = ref chunk.GetFirst(); foreach (int entityIndex in chunk) { - ref SceneDefinitionComponent definition = ref Unsafe.Add(ref sceneDefinitionFirst, entityIndex); ref readonly Entity entity = ref Unsafe.Add(ref entityFirstElement, entityIndex); ref PartitionComponent partitionComponent = ref Unsafe.Add(ref partitionComponentFirst, entityIndex); @@ -124,27 +139,37 @@ private void StartScenesLoading(ref RealmComponent realmComponent, float maxLoad orderedData.Add(new OrderedData { Entity = entity, - PartitionComponent = partitionComponent, - DefinitionComponent = definition, + Data = new DistanceBasedComparer.DataSurrogate(partitionComponent.RawSqrDistance, partitionComponent.IsBehind), }); } } // Raw Distance will give more stable results in terms of scenes loading order, especially in cases // when a wide range falls into the same bucket - orderedData.Sort(static (p1, p2) => DistanceBasedComparer.INSTANCE.Compare(p1.PartitionComponent, p2.PartitionComponent)); + sortingJobHandle = orderedData.SortJob(COMPARER_INSTANCE).Schedule(); + } - IIpfsRealm ipfsRealm = realmComponent.Ipfs; + private void CreatePromisesFromOrderedData(IIpfsRealm ipfsRealm) + { + var promisesCreated = 0; - // Take first N - for (var i = 0; i < orderedData.Count && i < realmPartitionSettings.ScenesRequestBatchSize; i++) + for (var i = 0; i < orderedData.Length && promisesCreated < realmPartitionSettings.ScenesRequestBatchSize; i++) { OrderedData data = orderedData[i]; + // As sorting is throttled Entity might gone out of scope + if (!World.IsAlive(data.Entity)) + continue; + + // We can't save component to data as sorting is throttled and components could change + Components components = World.Get(data.Entity); + World.Add(data.Entity, AssetPromise.Create(World, - new GetSceneFacadeIntention(ipfsRealm, data.DefinitionComponent), - data.PartitionComponent)); + new GetSceneFacadeIntention(ipfsRealm, components.t0), + components.t1.Value)); + + promisesCreated++; } } @@ -157,11 +182,22 @@ private void StartUnloading([Data] float unloadingSqrDistance, in Entity entity, World.Add(entity, DeleteEntityIntention.DeferredDeletion); } + /// + /// It must be a structure to be compatible with Burst SortJob + /// + private struct Comparer : IComparer + { + public int Compare(OrderedData x, OrderedData y) => + DistanceBasedComparer.Compare(x.Data, y.Data); + } + private struct OrderedData { - public PartitionComponent PartitionComponent; - public SceneDefinitionComponent DefinitionComponent; + /// + /// Referencing entity is expensive and at the moment we don't delete scene entities at all + /// public Entity Entity; + public DistanceBasedComparer.DataSurrogate Data; } } } diff --git a/Explorer/Assets/Scripts/ECS/SceneLifeCycle/Tests/ResolveSceneStateByIncreasingRadiusSystemShould.cs b/Explorer/Assets/Scripts/ECS/SceneLifeCycle/Tests/ResolveSceneStateByIncreasingRadiusSystemShould.cs index 171f56ccac..895928349a 100644 --- a/Explorer/Assets/Scripts/ECS/SceneLifeCycle/Tests/ResolveSceneStateByIncreasingRadiusSystemShould.cs +++ b/Explorer/Assets/Scripts/ECS/SceneLifeCycle/Tests/ResolveSceneStateByIncreasingRadiusSystemShould.cs @@ -12,6 +12,7 @@ using NUnit.Framework; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using UnityEngine; using Utility; @@ -32,7 +33,7 @@ public void SetUp() } [Test] - public void LimitScenesLoading() + public async Task LimitScenesLoading() { realmPartitionSettings.ScenesRequestBatchSize.Returns(2); realmPartitionSettings.MaxLoadingDistanceInParcels.Returns(int.MaxValue); @@ -56,6 +57,15 @@ public void LimitScenesLoading() system.Update(0f); + // Wait for job to complete + while (!system.sortingJobHandle.Value.IsCompleted) + { + await Task.Yield(); + system.Update(0f); + } + + system.Update(0f); + // Serve 2 var entities = new List(); world.GetEntities(new QueryDescription().WithAll(), entities); diff --git a/Explorer/Assets/Scripts/Utility/ParcelMathJobifiedHelper.cs b/Explorer/Assets/Scripts/Utility/ParcelMathJobifiedHelper.cs index d6915dec6a..8651c99206 100644 --- a/Explorer/Assets/Scripts/Utility/ParcelMathJobifiedHelper.cs +++ b/Explorer/Assets/Scripts/Utility/ParcelMathJobifiedHelper.cs @@ -1,4 +1,5 @@ -using Unity.Burst; +using System.Runtime.CompilerServices; +using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; @@ -29,11 +30,17 @@ private void EnsureRingsArraySize(int maxRadius) if (rings.Length != maxRadius) { rings.Dispose(); - int d = (maxRadius * 2) + 1; - rings = new NativeArray(d * d, Allocator.Persistent); + rings = new NativeArray(GetRingsArraySize(maxRadius), Allocator.Persistent); } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetRingsArraySize(int maxRadius) + { + int d = (maxRadius * 2) + 1; + return d * d; + } + /// /// Schedule a job so it can be completed later ///