// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Statistics; namespace osu.Framework.Graphics.Performance { /// /// Provides time-optimised updates for lifetime change notifications. /// This is used in specialised s to optimise lifetime changes (see: ). /// /// /// The time complexity of updating lifetimes is O(number of alive items). /// public class LifetimeEntryManager { /// /// Invoked immediately when a becomes alive. /// public event Action EntryBecameAlive; /// /// Invoked immediately when a becomes dead. /// public event Action EntryBecameDead; /// /// Invoked when a crosses a lifetime boundary. /// public event Action EntryCrossedBoundary; /// /// Contains all the newly-added (but not yet processed) entries. /// private readonly List newEntries = new List(); /// /// Contains all the currently-alive entries. /// private readonly List activeEntries = new List(); /// /// Contains all entries that should come alive in the future. /// private readonly SortedSet futureEntries = new SortedSet(new LifetimeStartComparator()); /// /// Contains all entries that were alive in the past. /// private readonly SortedSet pastEntries = new SortedSet(new LifetimeEndComparator()); private readonly Queue<(LifetimeEntry, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection)> eventQueue = new Queue<(LifetimeEntry, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection)>(); /// /// Used to ensure a stable sort if multiple entries with the same lifetime are added. /// private ulong currentChildId; /// /// Adds an entry. /// /// The to add. public void AddEntry(LifetimeEntry entry) { entry.RequestLifetimeUpdate += requestLifetimeUpdate; entry.ChildId = ++currentChildId; entry.State = LifetimeEntryState.New; newEntries.Add(entry); } /// /// Removes an entry. /// /// The to remove. /// Whether was contained by this . public bool RemoveEntry(LifetimeEntry entry) { entry.RequestLifetimeUpdate -= requestLifetimeUpdate; bool removed = false; switch (entry.State) { case LifetimeEntryState.New: removed = newEntries.Remove(entry); break; case LifetimeEntryState.Current: removed = activeEntries.Remove(entry); if (removed) EntryBecameDead?.Invoke(entry); break; case LifetimeEntryState.Past: // Past entries may be found in the newEntries set after a lifetime change (see requestLifetimeUpdate). removed = pastEntries.Remove(entry) || newEntries.Remove(entry); break; case LifetimeEntryState.Future: // Future entries may be found in the newEntries set after a lifetime change (see requestLifetimeUpdate). removed = futureEntries.Remove(entry) || newEntries.Remove(entry); break; } if (!removed) return false; entry.ChildId = 0; return true; } /// /// Removes all entries. /// public void ClearEntries() { foreach (var entry in newEntries) { entry.RequestLifetimeUpdate -= requestLifetimeUpdate; entry.ChildId = 0; } foreach (var entry in pastEntries) { entry.RequestLifetimeUpdate -= requestLifetimeUpdate; entry.ChildId = 0; } foreach (var entry in activeEntries) { entry.RequestLifetimeUpdate -= requestLifetimeUpdate; EntryBecameDead?.Invoke(entry); entry.ChildId = 0; } foreach (var entry in futureEntries) { entry.RequestLifetimeUpdate -= requestLifetimeUpdate; entry.ChildId = 0; } newEntries.Clear(); pastEntries.Clear(); activeEntries.Clear(); futureEntries.Clear(); } /// /// Invoked when the lifetime of an entry is going to changed. /// private void requestLifetimeUpdate(LifetimeEntry entry) { // Entries in the past/future sets need to be re-sorted to prevent the comparer from becoming unstable. // To prevent, e.g. CompositeDrawable alive children changing during enumeration, the entry's state must not be updated immediately. // // In order to achieve the above, the entry is first removed from the past/future set (resolving the comparator stability issues) // and then re-queued back onto the new entries list to be re-processed in the next Update(). // // Note that this does not apply to entries that are in the current set, as they don't utilise a lifetime comparer. var futureOrPastSet = futureOrPastEntries(entry.State); if (futureOrPastSet?.Remove(entry) == true) { // Enqueue the entry to be processed in the next Update(). newEntries.Add(entry); } } /// /// Retrieves the sorted set for a . /// /// The . /// Either , , or null. [CanBeNull] private SortedSet futureOrPastEntries(LifetimeEntryState state) { switch (state) { case LifetimeEntryState.Future: return futureEntries; case LifetimeEntryState.Past: return pastEntries; default: return null; } } /// /// Updates the lifetime of entries at a single time value. /// /// The time to update at. /// Whether the state of any entries changed. public bool Update(double time) => Update(time, time); /// /// Updates the lifetime of entries within a given time range. /// /// The start of the time range. /// The end of the time range. /// Whether the state of any entries changed. public bool Update(double startTime, double endTime) { endTime = Math.Max(endTime, startTime); bool aliveChildrenChanged = false; // Check for newly-added entries. foreach (var entry in newEntries) aliveChildrenChanged |= updateChildEntry(entry, startTime, endTime, true, true); newEntries.Clear(); // Check for newly alive entries when time is increased. while (futureEntries.Count > 0) { FrameStatistics.Increment(StatisticsCounterType.CCL); var entry = futureEntries.Min.AsNonNull(); // guaranteed by the .Count > 0 guard. Debug.Assert(entry.State == LifetimeEntryState.Future); if (getState(entry, startTime, endTime) == LifetimeEntryState.Future) break; futureEntries.Remove(entry); aliveChildrenChanged |= updateChildEntry(entry, startTime, endTime, false, true); } // Check for newly alive entries when time is decreased. while (pastEntries.Count > 0) { FrameStatistics.Increment(StatisticsCounterType.CCL); var entry = pastEntries.Max.AsNonNull(); // guaranteed by the .Count > 0 guard. Debug.Assert(entry.State == LifetimeEntryState.Past); if (getState(entry, startTime, endTime) == LifetimeEntryState.Past) break; pastEntries.Remove(entry); aliveChildrenChanged |= updateChildEntry(entry, startTime, endTime, false, true); } // Checks for newly dead entries when time is increased/decreased. foreach (var entry in activeEntries) { FrameStatistics.Increment(StatisticsCounterType.CCL); aliveChildrenChanged |= updateChildEntry(entry, startTime, endTime, false, false); } // Remove all newly-dead entries. activeEntries.RemoveAll(e => e.State != LifetimeEntryState.Current); while (eventQueue.Count != 0) { var (entry, kind, direction) = eventQueue.Dequeue(); EntryCrossedBoundary?.Invoke(entry, kind, direction); } return aliveChildrenChanged; } /// /// Updates the state of a single . /// /// The to update. /// The start of the time range. /// The end of the time range. /// Whether is part of the new entries set. /// The state may be "new" or "past"/"future", in which case it will undergo further processing to return it to the correct set. /// Whether should be mutated by this invocation. /// If false, the caller is expected to handle mutation of based on any changes to the entry's state. /// Whether the state of has changed. private bool updateChildEntry(LifetimeEntry entry, double startTime, double endTime, bool isNewEntry, bool mutateActiveEntries) { LifetimeEntryState oldState = entry.State; // Past/future sets don't call this function unless a state change is guaranteed. Debug.Assert(!futureEntries.Contains(entry) && !pastEntries.Contains(entry)); // The entry can be in one of three states: // 1. The entry was previously in the past/future sets but a lifetime change was requested. Its state is currently "past"/"future". // 2. The entry is a completely new entry. Its state is currently "new". // 3. The entry is currently-active. Its state is "current" but it's also in the active set. Debug.Assert(oldState != LifetimeEntryState.Current || activeEntries.Contains(entry)); LifetimeEntryState newState = getState(entry, startTime, endTime); Debug.Assert(newState != LifetimeEntryState.New); if (newState == oldState) { // If the state hasn't changed, then there's two possibilities: // 1. The entry was in the past/future sets and a lifetime change was requested. The entry needs to be added back to the past/future sets. // 2. The entry is and continues to remain active. if (isNewEntry) futureOrPastEntries(newState)?.Add(entry); else Debug.Assert(newState == LifetimeEntryState.Current); // In both cases, the entry doesn't need to be processed further as it's already in the correct state. return false; } bool aliveEntriesChanged = false; if (newState == LifetimeEntryState.Current) { if (mutateActiveEntries) activeEntries.Add(entry); EntryBecameAlive?.Invoke(entry); aliveEntriesChanged = true; } else if (oldState == LifetimeEntryState.Current) { if (mutateActiveEntries) activeEntries.Remove(entry); EntryBecameDead?.Invoke(entry); aliveEntriesChanged = true; } entry.State = newState; futureOrPastEntries(newState)?.Add(entry); enqueueEvents(entry, oldState, newState); return aliveEntriesChanged; } /// /// Retrieves the new state for an entry. /// /// The . /// The start of the time range. /// The end of the time range. /// The state of . Can be either , , or . private LifetimeEntryState getState(LifetimeEntry entry, double startTime, double endTime) { // Consider a static entry and a moving time range: // [-----------Entry-----------] // [----Range----] | | (not alive) // [----Range----] | (alive) // | [----Range----] (alive) // | [----Range----] (not alive) // | | [----Range----] (not alive) // | | if (endTime < entry.LifetimeStart) return LifetimeEntryState.Future; if (startTime >= entry.LifetimeEnd) return LifetimeEntryState.Past; return LifetimeEntryState.Current; } private void enqueueEvents(LifetimeEntry entry, LifetimeEntryState oldState, LifetimeEntryState newState) { Debug.Assert(oldState != newState); switch (oldState) { case LifetimeEntryState.Future: eventQueue.Enqueue((entry, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Forward)); if (newState == LifetimeEntryState.Past) eventQueue.Enqueue((entry, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Forward)); break; case LifetimeEntryState.Current: eventQueue.Enqueue(newState == LifetimeEntryState.Past ? (entry, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Forward) : (entry, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Backward)); break; case LifetimeEntryState.Past: eventQueue.Enqueue((entry, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Backward)); if (newState == LifetimeEntryState.Future) eventQueue.Enqueue((entry, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Backward)); break; } } /// /// Compares by . /// private sealed class LifetimeStartComparator : IComparer { public int Compare(LifetimeEntry x, LifetimeEntry y) { if (x == null) throw new ArgumentNullException(nameof(x)); if (y == null) throw new ArgumentNullException(nameof(y)); int c = x.LifetimeStart.CompareTo(y.LifetimeStart); return c != 0 ? c : x.ChildId.CompareTo(y.ChildId); } } /// /// Compares by . /// private sealed class LifetimeEndComparator : IComparer { public int Compare(LifetimeEntry x, LifetimeEntry y) { if (x == null) throw new ArgumentNullException(nameof(x)); if (y == null) throw new ArgumentNullException(nameof(y)); int c = x.LifetimeEnd.CompareTo(y.LifetimeEnd); return c != 0 ? c : x.ChildId.CompareTo(y.ChildId); } } } }