diff --git a/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs index 97aef452089..ab88938d499 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs @@ -298,6 +298,7 @@ public UniTask InitializeAsync(StaticSettings settings, CancellationToken ct) new AssetsCollidersPlugin(sharedDependencies), new AvatarShapePlugin(globalWorld, componentsContainer.ComponentPoolsRegistry, launchMode, useRemoteAssetBundles), new PrimitivesRenderingPlugin(sharedDependencies), + new SceneContentStatsPlugin(), new VisibilityPlugin(), new AudioSourcesPlugin(sharedDependencies, container.WebRequestsContainer.WebRequestController, container.CacheCleaner, container.assetsProvisioner), new AudioAnalysisPlugin(sharedDependencies), diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs index 1ba1f18fe67..2833fcb3b3d 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs @@ -151,7 +151,7 @@ public SceneInstanceDependencies( /* Pass dependencies here if they are needed by the systems */ ecsWorldSharedDependencies = new ECSWorldInstanceSharedDependencies(sceneData, partitionProvider, ecsToCRDTWriter, entitiesMap, ExceptionsHandler, EntityCollidersCache, SceneStateProvider, entityEventsBuilder, ecsMultiThreadSync, - systemGroupThrottler, systemsUpdateGate); + systemGroupThrottler, systemsUpdateGate, RuntimeMetrics); ECSWorldFacade = ecsWorldFactory.CreateWorld(new ECSWorldFactoryArgs(ecsWorldSharedDependencies, systemGroupThrottler, sceneData)); CRDTWorldSynchronizer = new CRDTWorldSynchronizer(ECSWorldFacade.EcsWorld, sdkComponentsRegistry, entityFactory, entitiesMap); diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ECS/DebugViewCurrentSceneSystem.Formatting.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ECS/DebugViewCurrentSceneSystem.Formatting.cs index 3cdc8dc72fc..4db2adda438 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ECS/DebugViewCurrentSceneSystem.Formatting.cs +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ECS/DebugViewCurrentSceneSystem.Formatting.cs @@ -1,7 +1,6 @@ using DCL.DebugUtilities; using DCL.DebugUtilities.UIBindings; -using DCL.Utilities; -using SceneRunner.Scene; +using System; using System.Globalization; using UnityEngine; @@ -9,6 +8,165 @@ namespace DCL.Profiling.ECS { public partial class DebugViewCurrentSceneSystem { + // Hardcoded scene limits derived from the parcel count (n), matching the documented + // Decentraland scene limitations; colliders and external content have no documented + // limit, so they use project-chosen caps. + private const int MAX_TRIANGLES_PER_PARCEL = 10_000; + private const int MAX_ENTITIES_PER_PARCEL = 200; + private const int MAX_BODIES_PER_PARCEL = 300; + private const int MAX_COLLIDERS_PER_PARCEL = 300; + private const int MAX_MATERIALS_LOG2_MULTIPLIER = 20; + private const int MAX_TEXTURES_LOG2_MULTIPLIER = 10; + private const int MAX_GEOMETRIES_LOG2_MULTIPLIER = 200; + private const long MAX_CONTENT_BYTES_PER_PARCEL = 15L * 1024 * 1024; + private const int MAX_EXTERNAL_CONTENT = 10; + + private const float CAP_WARNING_PERCENT = 80f; + + private readonly struct SceneContentCaps + { + public readonly int Entities; + public readonly long Triangles; + public readonly int Bodies; + public readonly int Geometries; + public readonly int Materials; + public readonly int Textures; + public readonly int Colliders; + public readonly long ContentSizeBytes; + public readonly int ExternalContent; + + private SceneContentCaps(int entities, long triangles, int bodies, int geometries, int materials, int textures, int colliders, long contentSizeBytes, int externalContent) + { + Entities = entities; + Triangles = triangles; + Bodies = bodies; + Geometries = geometries; + Materials = materials; + Textures = textures; + Colliders = colliders; + ContentSizeBytes = contentSizeBytes; + ExternalContent = externalContent; + } + + public static SceneContentCaps ForParcelCount(int parcelCount) + { + float log2 = Mathf.Log(parcelCount + 1, 2f); + + return new SceneContentCaps( + entities: parcelCount * MAX_ENTITIES_PER_PARCEL, + triangles: (long)parcelCount * MAX_TRIANGLES_PER_PARCEL, + bodies: parcelCount * MAX_BODIES_PER_PARCEL, + geometries: Mathf.FloorToInt(log2 * MAX_GEOMETRIES_LOG2_MULTIPLIER), + materials: Mathf.FloorToInt(log2 * MAX_MATERIALS_LOG2_MULTIPLIER), + textures: Mathf.FloorToInt(log2 * MAX_TEXTURES_LOG2_MULTIPLIER), + colliders: parcelCount * MAX_COLLIDERS_PER_PARCEL, + contentSizeBytes: parcelCount * MAX_CONTENT_BYTES_PER_PARCEL, + externalContent: MAX_EXTERNAL_CONTENT); + } + } + + private readonly struct ContentStatsBindings + { + public readonly ElementBinding Entities; + public readonly ElementBinding Triangles; + public readonly ElementBinding Bodies; + public readonly ElementBinding Geometries; + public readonly ElementBinding Materials; + public readonly ElementBinding Textures; + public readonly ElementBinding Colliders; + public readonly ElementBinding ContentSize; + public readonly ElementBinding ExternalContent; + + private ContentStatsBindings( + ElementBinding entities, + ElementBinding triangles, + ElementBinding bodies, + ElementBinding geometries, + ElementBinding materials, + ElementBinding textures, + ElementBinding colliders, + ElementBinding contentSize, + ElementBinding externalContent) + { + Entities = entities; + Triangles = triangles; + Bodies = bodies; + Geometries = geometries; + Materials = materials; + Textures = textures; + Colliders = colliders; + ContentSize = contentSize; + ExternalContent = externalContent; + } + + public static ContentStatsBindings Create() => + new ( + new ElementBinding(string.Empty), + new ElementBinding(string.Empty), + new ElementBinding(string.Empty), + new ElementBinding(string.Empty), + new ElementBinding(string.Empty), + new ElementBinding(string.Empty), + new ElementBinding(string.Empty), + new ElementBinding(string.Empty), + new ElementBinding(string.Empty)); + } + + private static void UpdateContentStatsBindings(in ContentStatsBindings bindings, SceneContentStats stats, in SceneContentCaps caps) + { + if (!stats.HasData) + { + bindings.Entities.Value = "—"; + bindings.Triangles.Value = "—"; + bindings.Bodies.Value = "—"; + bindings.Geometries.Value = "—"; + bindings.Materials.Value = "—"; + bindings.Textures.Value = "—"; + bindings.Colliders.Value = "—"; + bindings.ContentSize.Value = "—"; + bindings.ExternalContent.Value = "—"; + return; + } + + bindings.Entities.Value = FormatCapped(stats.Entities, caps.Entities); + bindings.Triangles.Value = FormatCapped(stats.Triangles, caps.Triangles); + bindings.Bodies.Value = FormatCapped(stats.Bodies, caps.Bodies); + bindings.Geometries.Value = FormatCapped(stats.Geometries, caps.Geometries); + bindings.Materials.Value = FormatCapped(stats.Materials, caps.Materials); + bindings.Textures.Value = FormatCapped(stats.Textures, caps.Textures); + bindings.Colliders.Value = FormatCapped(stats.Colliders, caps.Colliders); + bindings.ContentSize.Value = FormatCappedBytes(stats.ContentSizeBytes, caps.ContentSizeBytes); + bindings.ExternalContent.Value = FormatCapped(stats.ExternalContent, caps.ExternalContent); + } + + private static string FormatCapped(long current, long cap) + { + if (cap <= 0) + return current.ToString("N0", CultureInfo.InvariantCulture); + + float percent = current * 100f / cap; + return $"{current.ToString("N0", CultureInfo.InvariantCulture)} / {cap.ToString("N0", CultureInfo.InvariantCulture)} ({percent:F0}%)"; + } + + private static string FormatCappedBytes(long current, long cap) + { + string currentNormalized = BytesFormatter.Normalize((ulong)Math.Max(0L, current), false); + + if (cap <= 0) + return currentNormalized; + + float percent = current * 100f / cap; + return $"{currentNormalized} / {BytesFormatter.Normalize((ulong)cap, false)} ({percent:F0}%)"; + } + + private static string CapColor(float percent) => + percent switch + { + >= 100f => "red", + >= CAP_WARNING_PERCENT => "yellow", + _ => "green", + }; + private readonly struct StringBindings { public readonly ElementBinding RealFps; diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ECS/DebugViewCurrentSceneSystem.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ECS/DebugViewCurrentSceneSystem.cs index 8941d955a9e..57328ef4b24 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ECS/DebugViewCurrentSceneSystem.cs +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ECS/DebugViewCurrentSceneSystem.cs @@ -30,6 +30,7 @@ public partial class DebugViewCurrentSceneSystem : BaseUnityLoopSystem private readonly DebugWidgetVisibilityBinding visibility; private readonly StringBindings stringBindings; + private readonly ContentStatsBindings contentStatsBindings; private readonly ElementBinding fpsChart; private readonly ElementBinding bytesFromChart; @@ -50,6 +51,7 @@ public partial class DebugViewCurrentSceneSystem : BaseUnityLoopSystem private readonly Action? onCurrentSceneChanged; private ISceneFacade? currentScene; + private SceneContentCaps contentCaps; private long lastBytesFromScene; private long lastBytesToScene; private long lastMessagesFromScene; @@ -64,6 +66,7 @@ internal DebugViewCurrentSceneSystem(World world, IDebugContainerBuilder debugBu visibility = new DebugWidgetVisibilityBinding(true); stringBindings = StringBindings.Create(); + contentStatsBindings = ContentStatsBindings.Create(); fpsChart = new ElementBinding(new LineChartBuffer(fpsRing, 0, 0, 0)); bytesFromChart = new ElementBinding(new LineChartBuffer(bytesFromRing, 0, 0, 0)); @@ -83,6 +86,15 @@ internal DebugViewCurrentSceneSystem(World world, IDebugContainerBuilder debugBu OnCurrentSceneChanged(scenesCache.CurrentScene.Value); widgetBuilder.SetVisibilityBinding(visibility) + .AddCustomMarker("Entities:", contentStatsBindings.Entities) + .AddCustomMarker("Triangles:", contentStatsBindings.Triangles) + .AddCustomMarker("Meshes (bodies):", contentStatsBindings.Bodies) + .AddCustomMarker("Geometries:", contentStatsBindings.Geometries) + .AddCustomMarker("Materials:", contentStatsBindings.Materials) + .AddCustomMarker("Textures:", contentStatsBindings.Textures) + .AddCustomMarker("Colliders:", contentStatsBindings.Colliders) + .AddCustomMarker("Content size:", contentStatsBindings.ContentSize) + .AddCustomMarker("External content:", contentStatsBindings.ExternalContent) .AddCustomMarker("Real tick FPS:", stringBindings.RealFps) .AddCustomMarker("Min FPS (last 256 ticks):", stringBindings.MinFps) .AddCustomMarker("Max FPS (last 256 ticks):", stringBindings.MaxFps) @@ -110,10 +122,12 @@ protected override void Update(float t) { if (!widgetEnabled) return; if (!realmData.Configured) return; - if (!visibility.IsConnectedAndExpanded) return; if (currentScene == null) return; SceneRuntimeMetrics metrics = currentScene.RuntimeMetrics; + metrics.ContentStats.CollectionRequested = visibility.IsConnectedAndExpanded; + + if (!visibility.IsConnectedAndExpanded) return; long bytesFrom = metrics.BytesFromScene.Total; long bytesTo = metrics.BytesToScene.Total; @@ -150,6 +164,7 @@ protected override void Update(float t) framesSinceMetricsUpdate = 0; UpdateStringBindings(in stringBindings, metrics, currentFpsValue, minFpsValue, maxFpsValue, hiccupCount, deltaBytesFrom, deltaBytesTo, deltaMessagesFrom, deltaMessagesTo, dt); + UpdateContentStatsBindings(in contentStatsBindings, metrics.ContentStats, in contentCaps); } } @@ -161,7 +176,11 @@ protected override void OnDispose() private void OnCurrentSceneChanged(ISceneFacade? scene) { + if (currentScene != null) + currentScene.RuntimeMetrics.ContentStats.CollectionRequested = false; + currentScene = scene; + contentCaps = scene != null ? SceneContentCaps.ForParcelCount(scene.SceneData.Parcels.Count) : default(SceneContentCaps); ResetLocalState(); if (scene != null) diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/SceneContentStats.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/SceneContentStats.cs new file mode 100644 index 00000000000..2197e33f71b --- /dev/null +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/SceneContentStats.cs @@ -0,0 +1,31 @@ +namespace DCL.Profiling +{ + /// + /// Per-scene content statistics for the "Current scene" debug widget. Written by + /// SceneContentStatsSystem in the scene world and read by DebugViewCurrentSceneSystem + /// in the global world; both run on the Unity main thread. + /// + public sealed class SceneContentStats + { + /// + /// While false the scene world skips collection entirely, so the counters cost nothing + /// when the debug panel is disabled or the widget is collapsed. + /// + public bool CollectionRequested; + + /// + /// False until the first collection pass completes for this scene. + /// + public bool HasData; + + public int Entities; + public long Triangles; + public int Bodies; + public int Geometries; + public int Materials; + public int Textures; + public int Colliders; + public long ContentSizeBytes; + public int ExternalContent; + } +} diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/SceneContentStats.cs.meta b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/SceneContentStats.cs.meta new file mode 100644 index 00000000000..5941c6f26f1 --- /dev/null +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/SceneContentStats.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bcb7812a86404d96ae7d81de04540db9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/SceneRuntimeMetrics.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/SceneRuntimeMetrics.cs index 399240619d3..86402454e70 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/SceneRuntimeMetrics.cs +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/SceneRuntimeMetrics.cs @@ -13,6 +13,11 @@ public sealed class SceneRuntimeMetrics public readonly SampledCounter MessagesFromScene = new (); public readonly SampledCounter MessagesToScene = new (); + /// + /// Unlike the counters above, written and read on the Unity main thread only. + /// + public readonly SceneContentStats ContentStats = new (); + public int TargetFps { get; set; } } } diff --git a/Explorer/Assets/DCL/PluginSystem/World/Dependencies/ECSWorldInstanceSharedDependencies.cs b/Explorer/Assets/DCL/PluginSystem/World/Dependencies/ECSWorldInstanceSharedDependencies.cs index 2a89649dbe2..10406e4da3b 100644 --- a/Explorer/Assets/DCL/PluginSystem/World/Dependencies/ECSWorldInstanceSharedDependencies.cs +++ b/Explorer/Assets/DCL/PluginSystem/World/Dependencies/ECSWorldInstanceSharedDependencies.cs @@ -3,6 +3,7 @@ using CrdtEcsBridge.ECSToCRDTWriter; using CrdtEcsBridge.UpdateGate; using DCL.Interaction.Utility; +using DCL.Profiling; using ECS.Abstract; using ECS.Prioritization.Components; using SceneRunner.Scene; @@ -25,6 +26,7 @@ public readonly struct ECSWorldInstanceSharedDependencies public readonly MultiThreadSync MultiThreadSync; public readonly ISystemGroupsUpdateGate EcsGroupThrottler; public readonly ISystemsUpdateGate EcsSystemsGate; + public readonly SceneRuntimeMetrics RuntimeMetrics; public ECSWorldInstanceSharedDependencies( ISceneData sceneData, @@ -35,7 +37,8 @@ public ECSWorldInstanceSharedDependencies( IEntityCollidersSceneCache entityCollidersSceneCache, ISceneStateProvider sceneStateProvider, EntityEventsBuilder entityEventsBuilder, MultiThreadSync multiThreadSync, - ISystemGroupsUpdateGate ecsGroupThrottler, ISystemsUpdateGate ecsSystemsGate) + ISystemGroupsUpdateGate ecsGroupThrottler, ISystemsUpdateGate ecsSystemsGate, + SceneRuntimeMetrics runtimeMetrics) { SceneData = sceneData; EcsToCRDTWriter = ecsToCRDTWriter; @@ -48,6 +51,7 @@ public ECSWorldInstanceSharedDependencies( EcsGroupThrottler = ecsGroupThrottler; EcsSystemsGate = ecsSystemsGate; EntityEventsBuilder = entityEventsBuilder; + RuntimeMetrics = runtimeMetrics; } } } diff --git a/Explorer/Assets/DCL/PluginSystem/World/SceneContentStatsPlugin.cs b/Explorer/Assets/DCL/PluginSystem/World/SceneContentStatsPlugin.cs new file mode 100644 index 00000000000..62f905e3f02 --- /dev/null +++ b/Explorer/Assets/DCL/PluginSystem/World/SceneContentStatsPlugin.cs @@ -0,0 +1,16 @@ +using Arch.SystemGroups; +using DCL.PluginSystem.World.Dependencies; +using DCL.SDKComponents.SceneContentDebug.Systems; +using ECS.LifeCycle; +using System.Collections.Generic; + +namespace DCL.PluginSystem.World +{ + public class SceneContentStatsPlugin : IDCLWorldPluginWithoutSettings + { + public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in ECSWorldInstanceSharedDependencies sharedDependencies, in SystemsDependencies systemsDependencies, in PersistentEntities persistentEntities, List finalizeWorldSystems, List sceneIsCurrentListeners) + { + SceneContentStatsSystem.InjectToWorld(ref builder, sharedDependencies.RuntimeMetrics, sharedDependencies.EntitiesMap); + } + } +} diff --git a/Explorer/Assets/DCL/PluginSystem/World/SceneContentStatsPlugin.cs.meta b/Explorer/Assets/DCL/PluginSystem/World/SceneContentStatsPlugin.cs.meta new file mode 100644 index 00000000000..82ed3f818c5 --- /dev/null +++ b/Explorer/Assets/DCL/PluginSystem/World/SceneContentStatsPlugin.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0028115b35b74333bb8bd08a77cf4635 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/SDKComponents/SceneContentDebug.meta b/Explorer/Assets/DCL/SDKComponents/SceneContentDebug.meta new file mode 100644 index 00000000000..1fc23829935 --- /dev/null +++ b/Explorer/Assets/DCL/SDKComponents/SceneContentDebug.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 400cbaae27414423b8dfa154511c7067 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/SDKComponents/SceneContentDebug/Systems.meta b/Explorer/Assets/DCL/SDKComponents/SceneContentDebug/Systems.meta new file mode 100644 index 00000000000..74598cd8683 --- /dev/null +++ b/Explorer/Assets/DCL/SDKComponents/SceneContentDebug/Systems.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 45017b9ca6fb402dbec756fec4faf15b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/SDKComponents/SceneContentDebug/Systems/SceneContentStatsSystem.cs b/Explorer/Assets/DCL/SDKComponents/SceneContentDebug/Systems/SceneContentStatsSystem.cs new file mode 100644 index 00000000000..982ca2d6256 --- /dev/null +++ b/Explorer/Assets/DCL/SDKComponents/SceneContentDebug/Systems/SceneContentStatsSystem.cs @@ -0,0 +1,249 @@ +using Arch.Core; +using Arch.System; +using Arch.SystemGroups; +using CRDT; +using DCL.ECSComponents; +using DCL.Profiling; +using DCL.SDKComponents.MediaStream; +using DCL.SDKComponents.NFTShape.Component; +using ECS.Abstract; +using ECS.Groups; +using ECS.LifeCycle.Components; +using ECS.StreamableLoading.Common.Components; +using ECS.Unity.GLTFContainer.Asset.Components; +using ECS.Unity.GLTFContainer.Components; +using ECS.Unity.Materials.Components; +using ECS.Unity.PrimitiveColliders.Components; +using ECS.Unity.PrimitiveRenderer.Components; +using System.Collections.Generic; +using UnityEngine; +using UnityProfiler = UnityEngine.Profiling.Profiler; + +namespace DCL.SDKComponents.SceneContentDebug.Systems +{ + /// + /// Counts the scene's content (entities, triangles, meshes, geometries, materials, textures, + /// colliders, runtime content size and external content) into . + /// Collection is throttled and runs only while + /// is set, so the system is idle unless the "Current scene" debug widget is open on this scene. + /// + [UpdateInGroup(typeof(SyncedPresentationSystemGroup))] + public partial class SceneContentStatsSystem : BaseUnityLoopSystem + { + private const int COLLECTION_COOLDOWN_FRAMES = 30; + + private static readonly int[] TEXTURE_PROPERTY_IDS = + { + Shader.PropertyToID("_BaseMap"), + Shader.PropertyToID("_MainTex"), + Shader.PropertyToID("_BumpMap"), + Shader.PropertyToID("_EmissionMap"), + Shader.PropertyToID("_MetallicGlossMap"), + Shader.PropertyToID("_OcclusionMap"), + }; + + private static readonly QueryDescription NFT_SHAPES_QUERY = new QueryDescription() + .WithAll() + .WithNone(); + + private readonly SceneContentStats stats; + private readonly Dictionary entitiesMap; + + private readonly HashSet uniqueMeshes = new (); + private readonly HashSet uniqueMaterials = new (); + private readonly HashSet uniqueTextures = new (); + private readonly List materialsScratch = new (); + + private int framesSinceCollection = COLLECTION_COOLDOWN_FRAMES; + + private long triangles; + private int bodies; + private int geometries; + private int materials; + private int textures; + private int colliders; + private int externalContent; + private long contentSizeBytes; + + internal SceneContentStatsSystem(World world, SceneRuntimeMetrics runtimeMetrics, Dictionary entitiesMap) : base(world) + { + stats = runtimeMetrics.ContentStats; + this.entitiesMap = entitiesMap; + } + + protected override void Update(float t) + { + if (!stats.CollectionRequested) + { + // Keep the counter primed so reopening the widget collects on the very next frame, + // and drop asset references held from the last pass + framesSinceCollection = COLLECTION_COOLDOWN_FRAMES; + + if (uniqueMeshes.Count > 0) + { + uniqueMeshes.Clear(); + uniqueMaterials.Clear(); + uniqueTextures.Clear(); + } + + return; + } + + if (++framesSinceCollection < COLLECTION_COOLDOWN_FRAMES) return; + framesSinceCollection = 0; + + Collect(); + } + + private void Collect() + { + triangles = 0; + bodies = 0; + geometries = 0; + materials = 0; + textures = 0; + colliders = 0; + externalContent = 0; + contentSizeBytes = 0; + + uniqueMeshes.Clear(); + uniqueMaterials.Clear(); + uniqueTextures.Clear(); + + CountPrimitiveMeshesQuery(World); + CountGltfContainersQuery(World); + CountSdkMaterialsQuery(World); + CountPrimitiveCollidersQuery(World); + CountMediaStreamsQuery(World); + externalContent += World.CountEntities(in NFT_SHAPES_QUERY); + + stats.Entities = entitiesMap.Count; + stats.Triangles = triangles; + stats.Bodies = bodies; + stats.Geometries = geometries; + stats.Materials = materials; + stats.Textures = textures; + stats.Colliders = colliders; + stats.ExternalContent = externalContent; + stats.ContentSizeBytes = contentSizeBytes; + stats.HasData = true; + } + + [Query] + [None(typeof(DeleteEntityIntention))] + private void CountPrimitiveMeshes(in PrimitiveMeshRendererComponent component) + { + Mesh? mesh = component.PrimitiveMesh?.Mesh; + if (mesh == null) return; + + bodies++; + AccountMesh(mesh); + AccountRendererMaterials(component.MeshRenderer); + } + + [Query] + [None(typeof(DeleteEntityIntention))] + private void CountGltfContainers(in GltfContainerComponent component) + { + if (component.State != LoadingState.Finished) return; + + StreamableLoadingResult? result = component.Promise.Result; + if (result is not { Succeeded: true }) return; + + GltfContainerAsset asset = result.Value.Asset!; + + colliders += asset.InvisibleColliders.Count + (asset.DecodedVisibleSDKColliders?.Count ?? 0); + + List renderers = asset.Renderers; + + for (var i = 0; i < renderers.Count; i++) + { + Renderer renderer = renderers[i]; + + Mesh? mesh = renderer switch + { + SkinnedMeshRenderer skinned => skinned.sharedMesh, + _ => renderer.TryGetComponent(out MeshFilter meshFilter) ? meshFilter.sharedMesh : null, + }; + + bodies++; + + if (mesh != null) + AccountMesh(mesh); + + AccountRendererMaterials(renderer); + } + } + + [Query] + [None(typeof(DeleteEntityIntention))] + private void CountSdkMaterials(in MaterialComponent component) + { + if (component.Result != null) + AccountMaterial(component.Result); + } + + [Query] + [None(typeof(DeleteEntityIntention))] + private void CountPrimitiveColliders(in PrimitiveColliderComponent component) + { + if (component.Collider != null) + colliders++; + } + + [Query] + [None(typeof(DeleteEntityIntention))] + private void CountMediaStreams(in MediaPlayerComponent component) + { + if (!component.IsFromContentServer) + externalContent++; + } + + private void AccountMesh(Mesh mesh) + { + for (var i = 0; i < mesh.subMeshCount; i++) + triangles += mesh.GetIndexCount(i) / 3; + + if (uniqueMeshes.Add(mesh)) + { + geometries++; + contentSizeBytes += UnityProfiler.GetRuntimeMemorySizeLong(mesh); + } + } + + private void AccountRendererMaterials(Renderer renderer) + { + renderer.GetSharedMaterials(materialsScratch); + + for (var i = 0; i < materialsScratch.Count; i++) + { + Material material = materialsScratch[i]; + + if (material != null) + AccountMaterial(material); + } + } + + private void AccountMaterial(Material material) + { + if (!uniqueMaterials.Add(material)) return; + + materials++; + + for (var i = 0; i < TEXTURE_PROPERTY_IDS.Length; i++) + { + int propertyId = TEXTURE_PROPERTY_IDS[i]; + if (!material.HasProperty(propertyId)) continue; + + Texture? texture = material.GetTexture(propertyId); + if (texture == null) continue; + + if (uniqueTextures.Add(texture)) + { + textures++; + contentSizeBytes += UnityProfiler.GetRuntimeMemorySizeLong(texture); + } + } + } + } +} diff --git a/Explorer/Assets/DCL/SDKComponents/SceneContentDebug/Systems/SceneContentStatsSystem.cs.meta b/Explorer/Assets/DCL/SDKComponents/SceneContentDebug/Systems/SceneContentStatsSystem.cs.meta new file mode 100644 index 00000000000..542bbcc0346 --- /dev/null +++ b/Explorer/Assets/DCL/SDKComponents/SceneContentDebug/Systems/SceneContentStatsSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f69b6d03d54c43a1955ef424fe29e436 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: