Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b62f498
feat: progress is now reported with byte-weighted tracking
alejandro-jimenez-dcl Mar 18, 2026
7fcd792
fix: wip
alejandro-jimenez-dcl Mar 20, 2026
20f96d2
fix: update progress computation and handle unknown-size
alejandro-jimenez-dcl Apr 3, 2026
7628ed2
chore: code review
alejandro-jimenez-dcl Apr 8, 2026
66fa0d8
feat: convert the head request into a HeadOp to be part of the webreq…
alejandro-jimenez-dcl Apr 8, 2026
24002eb
Merge remote-tracking branch 'origin/dev' into feat/6849-loading-scre…
alejandro-jimenez-dcl May 15, 2026
dd5bb7d
refactor: simplify byte-weighted loading progress
alejandro-jimenez-dcl May 18, 2026
83b57cf
Merge branch 'dev' into feat/6849-loading-screen-by-data
alejandro-jimenez-dcl May 18, 2026
9e21fd7
refactor: skip ReadLoadingState for finished scene assets
alejandro-jimenez-dcl May 19, 2026
65c5125
feat: gate byte-weighted loading progress behind feature flag
alejandro-jimenez-dcl May 20, 2026
9f5b98f
fix: test compilation
alejandro-jimenez-dcl May 21, 2026
681ad3b
chore: missing meta
alejandro-jimenez-dcl May 21, 2026
5bb5cd8
refactor: simplify byte progress smoothing, add tracker tests
alejandro-jimenez-dcl May 21, 2026
b5bc906
fix: code convention
alejandro-jimenez-dcl May 21, 2026
6daec5c
feat: add feature flag
alejandro-jimenez-dcl May 21, 2026
d522d5c
Merge branch 'dev' into feat/6849-loading-screen-by-data
alejandro-jimenez-dcl May 22, 2026
5a26fef
Merge remote-tracking branch 'origin/dev' into feat/6849-loading-scre…
alejandro-jimenez-dcl Jun 4, 2026
f282fff
fix: handle HEAD timeout exceptions
alejandro-jimenez-dcl Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Explorer/Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public static class FeatureFlagsStrings
public const string AVATAR_CONTEXT_MENU = "alfa-avatar-context-menu";
public const string DOUBLE_CLICK_WALK = "alfa-double-click-walk";
public const string AB_DEPS_DIGEST_CACHE_KEY = "alfa-ab-deps-digest-cache-key";
public const string BYTE_WEIGHTED_LOADING_PROGRESS = "alfa-byte-weighted-loading-progress";

public static class Endpoints
{
Expand Down
2 changes: 2 additions & 0 deletions Explorer/Assets/DCL/FeatureFlags/FeaturesRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public FeaturesRegistry(
[FeatureId.AVATAR_CONTEXT_MENU] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.AVATAR_CONTEXT_MENU, featureFlags.IsEnabled(FeatureFlagsStrings.AVATAR_CONTEXT_MENU) || Application.isEditor),
[FeatureId.DOUBLE_CLICK_WALK] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.DOUBLE_CLICK_WALK, featureFlags.IsEnabled(FeatureFlagsStrings.DOUBLE_CLICK_WALK)),
[FeatureId.AB_DEPS_DIGEST_CACHE_KEY] = featureFlags.IsEnabled(FeatureFlagsStrings.AB_DEPS_DIGEST_CACHE_KEY),
[FeatureId.BYTE_WEIGHTED_LOADING_PROGRESS] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.BYTE_WEIGHTED_LOADING_PROGRESS, featureFlags.IsEnabled(FeatureFlagsStrings.BYTE_WEIGHTED_LOADING_PROGRESS) || isEditor),
// Note: COMMUNITIES feature is not cached here because it depends on user identity
});

Expand Down Expand Up @@ -197,5 +198,6 @@ public enum FeatureId
DOUBLE_CLICK_WALK = 61,
NEARBY_VOICE_CHAT = 62,
AB_DEPS_DIGEST_CACHE_KEY = 63,
BYTE_WEIGHTED_LOADING_PROGRESS = 64,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("DCL.EditMode.Tests")]

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using ECS.Abstract;
using ECS.Groups;
using ECS.SceneLifeCycle.Reporting;
using ECS.StreamableLoading.Common.Components;
using ECS.Unity.GLTFContainer.Components;
using ECS.Unity.Transforms.Components;
using ECS.Unity.Transforms.Systems;
Expand Down Expand Up @@ -47,6 +48,8 @@ public partial class GatherGltfAssetsSystem : BaseUnityLoopSystem
private readonly ILoadingStatus loadingStatus;
private readonly Entity sceneContainerEntity;

private SceneByteProgressTracker? progressTracker;

internal GatherGltfAssetsSystem(World world, ISceneReadinessReportQueue readinessReportQueue,
ISceneData sceneData, EntityEventBuffer<GltfContainerComponent> eventsBuffer,
ISceneStateProvider sceneStateProvider, MemoryBudget memoryBudget,
Expand All @@ -67,6 +70,7 @@ internal GatherGltfAssetsSystem(World world, ISceneReadinessReportQueue readines
public override void Initialize()
{
entitiesUnderObservation = HashSetPool<Entity>.Get();
progressTracker = new SceneByteProgressTracker();
startTime = Time.time;
}

Expand All @@ -77,6 +81,10 @@ protected override void OnDispose()
HashSetPool<Entity>.Release(entitiesUnderObservation);
entitiesUnderObservation = null;
}

progressTracker?.Dispose();
progressTracker = null;

sceneData.SceneLoadingConcluded = true;
}

Expand Down Expand Up @@ -111,21 +119,27 @@ protected override void Update(float t)
if (!World.IsAlive(entityRef)
|| !World.TryGet(entityRef, out GltfContainerComponent gltfContainerComponent))
{
progressTracker!.CreditDeath(entityRef);
toDelete.Add(entityRef);
continue;
}

// if Gltf Container Component has finished loading at least once (it can be reconfigured, we don't care)
if (gltfContainerComponent.State == LoadingState.Loading)
// if at least one entity is still loading, we are not done.
{
(long contentLength, float entityProgress) = ReadLoadingState(in gltfContainerComponent);
progressTracker!.RegisterIfNew(entityRef, contentLength);
concluded = false;
progressTracker.AccumulateInProgress(entityProgress, contentLength);
}
else
// remove entity from list - it's loaded, we don't need to check it anymore
{
progressTracker!.CreditFinish(entityRef);
toDelete.Add(entityRef);
}
}

assetsResolved += toDelete.Count;
float progress = totalAssetsToResolve != 0 ? assetsResolved / (float)totalAssetsToResolve : 1;
float progress = progressTracker!.ComputeAndClamp(totalAssetsToResolve, t);

for (var i = 0; i < reports!.Value.Count; i++)
{
Expand Down Expand Up @@ -154,6 +168,12 @@ protected override void Update(float t)

if (concluded)
{
for (var i = 0; i < reports!.Value.Count; i++)
{
AsyncLoadProcessReport report = reports.Value[i];
report.SetProgress(1);
}

reports.Value.Dispose();
reports = null;
Conclude();
Expand All @@ -172,6 +192,15 @@ void Conclude()
}
}

private (long contentLength, float progress) ReadLoadingState(in GltfContainerComponent gltfContainerComponent)
{
Entity promiseEntity = gltfContainerComponent.Promise.Entity;
if (!World.IsAlive(promiseEntity) || !World.TryGet(promiseEntity, out StreamableLoadingState loadingState))
return (0, 0f);

return (loadingState.ContentLength, loadingState.Progress);
}

private void GatherEntities(Entity entity, GltfContainerComponent component)
{
// No matter to which state component has changed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Arch.Core;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;

namespace ECS.SceneLifeCycle.Systems
{
/// <summary>
/// Byte-weighted progress accumulator for a scene's GLTF assets.
/// Tracks per-entity ContentLength and weights assets whose size was never observed
/// as <see cref="UNKNOWN_ASSET_BYTES"/> so they still contribute a fixed share to total progress.
/// </summary>
internal class SceneByteProgressTracker : IDisposable
{
// Flat weight for assets with no observed ContentLength (cache hits, HEAD failures).
// Kept at 1 byte so unknowns don't skew the bar away from real-download progress: cache hits
// are effectively free, just held by finalize budget, and shouldn't drag the percentage down.
private const long UNKNOWN_ASSET_BYTES = 1;

private readonly Dictionary<Entity, long> sizes;

private long completedBytes;
private long totalBytesExpected;
private int entitiesWithKnownSize;

// Per-frame accumulator: weighted bytes for entities still loading.
// Reset by ComputeAndClamp at frame end.
private long inProgressWeightedBytes;

// Byte-weighted math can dip when a small entity finishes while a new large unknown entity registers.
// Clamping locally avoids visual regression without affecting other AsyncLoadProcessReport callers (e.g. teleport retry).
private float maxReportedProgress;

public SceneByteProgressTracker()
{
sizes = DictionaryPool<Entity, long>.Get();
}

public void Dispose()
{
DictionaryPool<Entity, long>.Release(sizes);
}

/// <summary>
/// First sighting with known length: accumulate ContentLength into totalBytesExpected exactly once
/// and remember it so the finish path doesn't need to re-read the loading state.
/// Idempotent for already-tracked entities or unknown sizes.
/// </summary>
public void RegisterIfNew(Entity entity, long contentLength)
{
if (contentLength > 0 && sizes.TryAdd(entity, contentLength))
{
totalBytesExpected += contentLength;
entitiesWithKnownSize++;
}
}

/// <summary>
/// Clean finish: credit the entity's stored ContentLength from registration,
/// or <see cref="UNKNOWN_ASSET_BYTES"/> if its size was never observed.
/// </summary>
public void CreditFinish(Entity entity)
{
if (sizes.Remove(entity, out long contentLength))
completedBytes += contentLength;
else
completedBytes += UNKNOWN_ASSET_BYTES;
}

/// <summary>
/// Sudden death (no clean finish observed): credit the entity's registered ContentLength so
/// totalBytesExpected stays balanced. No-op if the entity was never registered.
/// </summary>
public void CreditDeath(Entity entity)
{
if (sizes.Remove(entity, out long contentLength))
completedBytes += contentLength;
}

/// <summary>
/// Accumulate weighted in-progress bytes for an entity that is still loading this frame.
/// </summary>
public void AccumulateInProgress(float entityProgress, long contentLength)
{
if (contentLength > 0)
inProgressWeightedBytes += (long)(entityProgress * contentLength);
}

/// <summary>
/// Returns this frame's progress value, guaranteed never to go down between frames.
/// Call once per frame: it also clears the per-frame in-progress accumulator.
/// </summary>
public float ComputeAndClamp(int totalAssetsToResolve, float deltaTime)
{
// Frame-rate independent smoothing: τ ≈ 111ms matches 0.15-per-tick at 60fps.
const float SMOOTHING_TIME_CONSTANT = 0.111f;

float target = Compute(totalAssetsToResolve);
inProgressWeightedBytes = 0;
if (target > maxReportedProgress)
{
float alpha = Mathf.Clamp01(deltaTime / SMOOTHING_TIME_CONSTANT);
maxReportedProgress = Mathf.Lerp(maxReportedProgress, target, alpha);
}
return maxReportedProgress;
}

// Cap below 1.0 because AsyncLoadProcessReport.SetProgress(>=1f) auto-resolves its completion source,
// which closes the loading screen. In-progress credits can saturate effectiveTotal during the finalize-wait
// window (entities are downloaded but still in LoadingState.Loading); only the explicit conclude path
// in GatherGltfAssetsSystem should report 1.0.
private const float MAX_IN_PROGRESS = 0.99f;

private float Compute(int totalAssetsToResolve)
{
int unknownCount = Math.Max(0, totalAssetsToResolve - entitiesWithKnownSize);
long effectiveTotal = totalBytesExpected + (UNKNOWN_ASSET_BYTES * unknownCount);

return effectiveTotal > 0
? Mathf.Min(MAX_IN_PROGRESS, (float)(completedBytes + inProgressWeightedBytes) / effectiveTotal)
: 0f;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading