1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-19 07:42:58 +08:00

Separate Lifetime computation and layout update

This commit is contained in:
ekrctb 2020-11-24 16:06:01 +09:00
parent ec92545d7a
commit ce57e8ddfb

View File

@ -2,13 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Framework.Threading;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
@ -19,7 +16,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
private readonly IBindable<double> timeRange = new BindableDouble(); private readonly IBindable<double> timeRange = new BindableDouble();
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly Dictionary<DrawableHitObject, InitialState> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, InitialState>();
// If a hit object is not in this set, the position and the size should be updated when the hit object becomes alive.
private readonly HashSet<DrawableHitObject> layoutComputedHitObjects = new HashSet<DrawableHitObject>();
// Used to recompute all lifetime when `layoutCache` becomes invalid
private readonly HashSet<DrawableHitObject> lifetimeComputedHitObjects = new HashSet<DrawableHitObject>();
[Resolved] [Resolved]
private IScrollingInfo scrollingInfo { get; set; } private IScrollingInfo scrollingInfo { get; set; }
@ -27,10 +29,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Responds to changes in the layout. When the layout changes, all hit object states must be recomputed. // Responds to changes in the layout. When the layout changes, all hit object states must be recomputed.
private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo); private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
// A combined cache across all hit object states to reduce per-update iterations.
// When invalidated, one or more (but not necessarily all) hitobject states must be re-validated.
private readonly Cached combinedObjCache = new Cached();
public ScrollingHitObjectContainer() public ScrollingHitObjectContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -52,8 +50,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
base.Clear(disposeChildren); base.Clear(disposeChildren);
combinedObjCache.Invalidate(); layoutComputedHitObjects.Clear();
hitObjectInitialStateCache.Clear();
} }
/// <summary> /// <summary>
@ -150,13 +147,15 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// <summary> /// <summary>
/// Invalidate the cache of the layout of this hit object. /// Invalidate the cache of the layout of this hit object.
/// A hit object should be invalidated after all its nested hit objects are invalidated.
/// </summary> /// </summary>
public void InvalidateDrawableHitObject(DrawableHitObject drawableObject) public void InvalidateDrawableHitObject(DrawableHitObject hitObject)
{ {
if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state)) // lifetime is computed before update
state.Cache.Invalidate(); hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
combinedObjCache.Invalidate(); lifetimeComputedHitObjects.Add(hitObject);
layoutComputedHitObjects.Remove(hitObject);
} }
// Use a nonzero value to prevent infinite results // Use a nonzero value to prevent infinite results
@ -168,17 +167,14 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (!layoutCache.IsValid) if (!layoutCache.IsValid)
{ {
foreach (var state in hitObjectInitialStateCache.Values) // this.Objects cannot be used as it doesn't contain nested objects
state.Cache.Invalidate(); foreach (var hitObject in lifetimeComputedHitObjects)
combinedObjCache.Invalidate(); hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
layoutComputedHitObjects.Clear();
scrollingInfo.Algorithm.Reset(); scrollingInfo.Algorithm.Reset();
layoutCache.Validate();
}
if (!combinedObjCache.IsValid)
{
switch (direction.Value) switch (direction.Value)
{ {
case ScrollingDirection.Up: case ScrollingDirection.Up:
@ -191,32 +187,18 @@ namespace osu.Game.Rulesets.UI.Scrolling
break; break;
} }
foreach (var obj in Objects) layoutCache.Validate();
{
if (!hitObjectInitialStateCache.TryGetValue(obj, out var state))
state = hitObjectInitialStateCache[obj] = new InitialState(new Cached());
if (state.Cache.IsValid)
continue;
state.ScheduledComputation?.Cancel();
state.ScheduledComputation = computeInitialStateRecursive(obj);
computeLifetimeStartRecursive(obj);
state.Cache.Validate();
}
combinedObjCache.Validate();
} }
}
private void computeLifetimeStartRecursive(DrawableHitObject hitObject) foreach (var obj in AliveObjects)
{ {
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); if (layoutComputedHitObjects.Contains(obj))
continue;
foreach (var obj in hitObject.NestedHitObjects) updateLayoutRecursive(obj);
computeLifetimeStartRecursive(obj);
layoutComputedHitObjects.Add(obj);
}
} }
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
@ -247,7 +229,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
} }
private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => private void updateLayoutRecursive(DrawableHitObject hitObject)
{ {
if (hitObject.HitObject is IHasDuration e) if (hitObject.HitObject is IHasDuration e)
{ {
@ -267,12 +249,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
foreach (var obj in hitObject.NestedHitObjects) foreach (var obj in hitObject.NestedHitObjects)
{ {
computeInitialStateRecursive(obj); updateLayoutRecursive(obj);
// Nested hitobjects don't need to scroll, but they do need accurate positions // Nested hitobjects don't need to scroll, but they do need accurate positions
updatePosition(obj, hitObject.HitObject.StartTime); updatePosition(obj, hitObject.HitObject.StartTime);
} }
}); }
protected override void UpdateAfterChildrenLife() protected override void UpdateAfterChildrenLife()
{ {
@ -304,19 +286,5 @@ namespace osu.Game.Rulesets.UI.Scrolling
break; break;
} }
} }
private class InitialState
{
[NotNull]
public readonly Cached Cache;
[CanBeNull]
public ScheduledDelegate ScheduledComputation;
public InitialState(Cached cache)
{
Cache = cache;
}
}
} }
} }