A game framework written with osu! in mind.
at master 424 lines 18 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections.Generic; 6using System.Diagnostics; 7using JetBrains.Annotations; 8using osu.Framework.Extensions.ObjectExtensions; 9using osu.Framework.Graphics.Containers; 10using osu.Framework.Statistics; 11 12namespace osu.Framework.Graphics.Performance 13{ 14 /// <summary> 15 /// Provides time-optimised updates for lifetime change notifications. 16 /// This is used in specialised <see cref="CompositeDrawable"/>s to optimise lifetime changes (see: <see cref="LifetimeManagementContainer"/>). 17 /// </summary> 18 /// <remarks> 19 /// The time complexity of updating lifetimes is O(number of alive items). 20 /// </remarks> 21 public class LifetimeEntryManager 22 { 23 /// <summary> 24 /// Invoked immediately when a <see cref="LifetimeEntry"/> becomes alive. 25 /// </summary> 26 public event Action<LifetimeEntry> EntryBecameAlive; 27 28 /// <summary> 29 /// Invoked immediately when a <see cref="LifetimeEntry"/> becomes dead. 30 /// </summary> 31 public event Action<LifetimeEntry> EntryBecameDead; 32 33 /// <summary> 34 /// Invoked when a <see cref="LifetimeEntry"/> crosses a lifetime boundary. 35 /// </summary> 36 public event Action<LifetimeEntry, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection> EntryCrossedBoundary; 37 38 /// <summary> 39 /// Contains all the newly-added (but not yet processed) entries. 40 /// </summary> 41 private readonly List<LifetimeEntry> newEntries = new List<LifetimeEntry>(); 42 43 /// <summary> 44 /// Contains all the currently-alive entries. 45 /// </summary> 46 private readonly List<LifetimeEntry> activeEntries = new List<LifetimeEntry>(); 47 48 /// <summary> 49 /// Contains all entries that should come alive in the future. 50 /// </summary> 51 private readonly SortedSet<LifetimeEntry> futureEntries = new SortedSet<LifetimeEntry>(new LifetimeStartComparator()); 52 53 /// <summary> 54 /// Contains all entries that were alive in the past. 55 /// </summary> 56 private readonly SortedSet<LifetimeEntry> pastEntries = new SortedSet<LifetimeEntry>(new LifetimeEndComparator()); 57 58 private readonly Queue<(LifetimeEntry, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection)> eventQueue = 59 new Queue<(LifetimeEntry, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection)>(); 60 61 /// <summary> 62 /// Used to ensure a stable sort if multiple entries with the same lifetime are added. 63 /// </summary> 64 private ulong currentChildId; 65 66 /// <summary> 67 /// Adds an entry. 68 /// </summary> 69 /// <param name="entry">The <see cref="LifetimeEntry"/> to add.</param> 70 public void AddEntry(LifetimeEntry entry) 71 { 72 entry.RequestLifetimeUpdate += requestLifetimeUpdate; 73 entry.ChildId = ++currentChildId; 74 entry.State = LifetimeEntryState.New; 75 76 newEntries.Add(entry); 77 } 78 79 /// <summary> 80 /// Removes an entry. 81 /// </summary> 82 /// <param name="entry">The <see cref="LifetimeEntry"/> to remove.</param> 83 /// <returns>Whether <paramref name="entry"/> was contained by this <see cref="LifetimeEntryManager"/>.</returns> 84 public bool RemoveEntry(LifetimeEntry entry) 85 { 86 entry.RequestLifetimeUpdate -= requestLifetimeUpdate; 87 88 bool removed = false; 89 90 switch (entry.State) 91 { 92 case LifetimeEntryState.New: 93 removed = newEntries.Remove(entry); 94 break; 95 96 case LifetimeEntryState.Current: 97 removed = activeEntries.Remove(entry); 98 99 if (removed) 100 EntryBecameDead?.Invoke(entry); 101 102 break; 103 104 case LifetimeEntryState.Past: 105 // Past entries may be found in the newEntries set after a lifetime change (see requestLifetimeUpdate). 106 removed = pastEntries.Remove(entry) || newEntries.Remove(entry); 107 break; 108 109 case LifetimeEntryState.Future: 110 // Future entries may be found in the newEntries set after a lifetime change (see requestLifetimeUpdate). 111 removed = futureEntries.Remove(entry) || newEntries.Remove(entry); 112 break; 113 } 114 115 if (!removed) 116 return false; 117 118 entry.ChildId = 0; 119 return true; 120 } 121 122 /// <summary> 123 /// Removes all entries. 124 /// </summary> 125 public void ClearEntries() 126 { 127 foreach (var entry in newEntries) 128 { 129 entry.RequestLifetimeUpdate -= requestLifetimeUpdate; 130 entry.ChildId = 0; 131 } 132 133 foreach (var entry in pastEntries) 134 { 135 entry.RequestLifetimeUpdate -= requestLifetimeUpdate; 136 entry.ChildId = 0; 137 } 138 139 foreach (var entry in activeEntries) 140 { 141 entry.RequestLifetimeUpdate -= requestLifetimeUpdate; 142 EntryBecameDead?.Invoke(entry); 143 entry.ChildId = 0; 144 } 145 146 foreach (var entry in futureEntries) 147 { 148 entry.RequestLifetimeUpdate -= requestLifetimeUpdate; 149 entry.ChildId = 0; 150 } 151 152 newEntries.Clear(); 153 pastEntries.Clear(); 154 activeEntries.Clear(); 155 futureEntries.Clear(); 156 } 157 158 /// <summary> 159 /// Invoked when the lifetime of an entry is going to changed. 160 /// </summary> 161 private void requestLifetimeUpdate(LifetimeEntry entry) 162 { 163 // Entries in the past/future sets need to be re-sorted to prevent the comparer from becoming unstable. 164 // To prevent, e.g. CompositeDrawable alive children changing during enumeration, the entry's state must not be updated immediately. 165 // 166 // In order to achieve the above, the entry is first removed from the past/future set (resolving the comparator stability issues) 167 // and then re-queued back onto the new entries list to be re-processed in the next Update(). 168 // 169 // Note that this does not apply to entries that are in the current set, as they don't utilise a lifetime comparer. 170 171 var futureOrPastSet = futureOrPastEntries(entry.State); 172 173 if (futureOrPastSet?.Remove(entry) == true) 174 { 175 // Enqueue the entry to be processed in the next Update(). 176 newEntries.Add(entry); 177 } 178 } 179 180 /// <summary> 181 /// Retrieves the sorted set for a <see cref="LifetimeEntryState"/>. 182 /// </summary> 183 /// <param name="state">The <see cref="LifetimeEntryState"/>.</param> 184 /// <returns>Either <see cref="futureEntries"/>, <see cref="pastEntries"/>, or null.</returns> 185 [CanBeNull] 186 private SortedSet<LifetimeEntry> futureOrPastEntries(LifetimeEntryState state) 187 { 188 switch (state) 189 { 190 case LifetimeEntryState.Future: 191 return futureEntries; 192 193 case LifetimeEntryState.Past: 194 return pastEntries; 195 196 default: 197 return null; 198 } 199 } 200 201 /// <summary> 202 /// Updates the lifetime of entries at a single time value. 203 /// </summary> 204 /// <param name="time">The time to update at.</param> 205 /// <returns>Whether the state of any entries changed.</returns> 206 public bool Update(double time) => Update(time, time); 207 208 /// <summary> 209 /// Updates the lifetime of entries within a given time range. 210 /// </summary> 211 /// <param name="startTime">The start of the time range.</param> 212 /// <param name="endTime">The end of the time range.</param> 213 /// <returns>Whether the state of any entries changed.</returns> 214 public bool Update(double startTime, double endTime) 215 { 216 endTime = Math.Max(endTime, startTime); 217 218 bool aliveChildrenChanged = false; 219 220 // Check for newly-added entries. 221 foreach (var entry in newEntries) 222 aliveChildrenChanged |= updateChildEntry(entry, startTime, endTime, true, true); 223 newEntries.Clear(); 224 225 // Check for newly alive entries when time is increased. 226 while (futureEntries.Count > 0) 227 { 228 FrameStatistics.Increment(StatisticsCounterType.CCL); 229 230 var entry = futureEntries.Min.AsNonNull(); // guaranteed by the .Count > 0 guard. 231 Debug.Assert(entry.State == LifetimeEntryState.Future); 232 233 if (getState(entry, startTime, endTime) == LifetimeEntryState.Future) 234 break; 235 236 futureEntries.Remove(entry); 237 aliveChildrenChanged |= updateChildEntry(entry, startTime, endTime, false, true); 238 } 239 240 // Check for newly alive entries when time is decreased. 241 while (pastEntries.Count > 0) 242 { 243 FrameStatistics.Increment(StatisticsCounterType.CCL); 244 245 var entry = pastEntries.Max.AsNonNull(); // guaranteed by the .Count > 0 guard. 246 Debug.Assert(entry.State == LifetimeEntryState.Past); 247 248 if (getState(entry, startTime, endTime) == LifetimeEntryState.Past) 249 break; 250 251 pastEntries.Remove(entry); 252 aliveChildrenChanged |= updateChildEntry(entry, startTime, endTime, false, true); 253 } 254 255 // Checks for newly dead entries when time is increased/decreased. 256 foreach (var entry in activeEntries) 257 { 258 FrameStatistics.Increment(StatisticsCounterType.CCL); 259 aliveChildrenChanged |= updateChildEntry(entry, startTime, endTime, false, false); 260 } 261 262 // Remove all newly-dead entries. 263 activeEntries.RemoveAll(e => e.State != LifetimeEntryState.Current); 264 265 while (eventQueue.Count != 0) 266 { 267 var (entry, kind, direction) = eventQueue.Dequeue(); 268 EntryCrossedBoundary?.Invoke(entry, kind, direction); 269 } 270 271 return aliveChildrenChanged; 272 } 273 274 /// <summary> 275 /// Updates the state of a single <see cref="LifetimeEntry"/>. 276 /// </summary> 277 /// <param name="entry">The <see cref="LifetimeEntry"/> to update.</param> 278 /// <param name="startTime">The start of the time range.</param> 279 /// <param name="endTime">The end of the time range.</param> 280 /// <param name="isNewEntry">Whether <paramref name="entry"/> is part of the new entries set. 281 /// The state may be "new" or "past"/"future", in which case it will undergo further processing to return it to the correct set.</param> 282 /// <param name="mutateActiveEntries">Whether <see cref="activeEntries"/> should be mutated by this invocation. 283 /// If <c>false</c>, the caller is expected to handle mutation of <see cref="activeEntries"/> based on any changes to the entry's state.</param> 284 /// <returns>Whether the state of <paramref name="entry"/> has changed.</returns> 285 private bool updateChildEntry(LifetimeEntry entry, double startTime, double endTime, bool isNewEntry, bool mutateActiveEntries) 286 { 287 LifetimeEntryState oldState = entry.State; 288 289 // Past/future sets don't call this function unless a state change is guaranteed. 290 Debug.Assert(!futureEntries.Contains(entry) && !pastEntries.Contains(entry)); 291 292 // The entry can be in one of three states: 293 // 1. The entry was previously in the past/future sets but a lifetime change was requested. Its state is currently "past"/"future". 294 // 2. The entry is a completely new entry. Its state is currently "new". 295 // 3. The entry is currently-active. Its state is "current" but it's also in the active set. 296 Debug.Assert(oldState != LifetimeEntryState.Current || activeEntries.Contains(entry)); 297 298 LifetimeEntryState newState = getState(entry, startTime, endTime); 299 Debug.Assert(newState != LifetimeEntryState.New); 300 301 if (newState == oldState) 302 { 303 // If the state hasn't changed, then there's two possibilities: 304 // 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. 305 // 2. The entry is and continues to remain active. 306 if (isNewEntry) 307 futureOrPastEntries(newState)?.Add(entry); 308 else 309 Debug.Assert(newState == LifetimeEntryState.Current); 310 311 // In both cases, the entry doesn't need to be processed further as it's already in the correct state. 312 return false; 313 } 314 315 bool aliveEntriesChanged = false; 316 317 if (newState == LifetimeEntryState.Current) 318 { 319 if (mutateActiveEntries) 320 activeEntries.Add(entry); 321 322 EntryBecameAlive?.Invoke(entry); 323 aliveEntriesChanged = true; 324 } 325 else if (oldState == LifetimeEntryState.Current) 326 { 327 if (mutateActiveEntries) 328 activeEntries.Remove(entry); 329 330 EntryBecameDead?.Invoke(entry); 331 aliveEntriesChanged = true; 332 } 333 334 entry.State = newState; 335 futureOrPastEntries(newState)?.Add(entry); 336 enqueueEvents(entry, oldState, newState); 337 338 return aliveEntriesChanged; 339 } 340 341 /// <summary> 342 /// Retrieves the new state for an entry. 343 /// </summary> 344 /// <param name="entry">The <see cref="LifetimeEntry"/>.</param> 345 /// <param name="startTime">The start of the time range.</param> 346 /// <param name="endTime">The end of the time range.</param> 347 /// <returns>The state of <paramref name="entry"/>. Can be either <see cref="LifetimeEntryState.Past"/>, <see cref="LifetimeEntryState.Current"/>, or <see cref="LifetimeEntryState.Future"/>.</returns> 348 private LifetimeEntryState getState(LifetimeEntry entry, double startTime, double endTime) 349 { 350 // Consider a static entry and a moving time range: 351 // [-----------Entry-----------] 352 // [----Range----] | | (not alive) 353 // [----Range----] | (alive) 354 // | [----Range----] (alive) 355 // | [----Range----] (not alive) 356 // | | [----Range----] (not alive) 357 // | | 358 359 if (endTime < entry.LifetimeStart) 360 return LifetimeEntryState.Future; 361 362 if (startTime >= entry.LifetimeEnd) 363 return LifetimeEntryState.Past; 364 365 return LifetimeEntryState.Current; 366 } 367 368 private void enqueueEvents(LifetimeEntry entry, LifetimeEntryState oldState, LifetimeEntryState newState) 369 { 370 Debug.Assert(oldState != newState); 371 372 switch (oldState) 373 { 374 case LifetimeEntryState.Future: 375 eventQueue.Enqueue((entry, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Forward)); 376 if (newState == LifetimeEntryState.Past) 377 eventQueue.Enqueue((entry, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Forward)); 378 break; 379 380 case LifetimeEntryState.Current: 381 eventQueue.Enqueue(newState == LifetimeEntryState.Past 382 ? (entry, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Forward) 383 : (entry, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Backward)); 384 break; 385 386 case LifetimeEntryState.Past: 387 eventQueue.Enqueue((entry, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Backward)); 388 if (newState == LifetimeEntryState.Future) 389 eventQueue.Enqueue((entry, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Backward)); 390 break; 391 } 392 } 393 394 /// <summary> 395 /// Compares by <see cref="LifetimeEntry.LifetimeStart"/>. 396 /// </summary> 397 private sealed class LifetimeStartComparator : IComparer<LifetimeEntry> 398 { 399 public int Compare(LifetimeEntry x, LifetimeEntry y) 400 { 401 if (x == null) throw new ArgumentNullException(nameof(x)); 402 if (y == null) throw new ArgumentNullException(nameof(y)); 403 404 int c = x.LifetimeStart.CompareTo(y.LifetimeStart); 405 return c != 0 ? c : x.ChildId.CompareTo(y.ChildId); 406 } 407 } 408 409 /// <summary> 410 /// Compares by <see cref="LifetimeEntry.LifetimeEnd"/>. 411 /// </summary> 412 private sealed class LifetimeEndComparator : IComparer<LifetimeEntry> 413 { 414 public int Compare(LifetimeEntry x, LifetimeEntry y) 415 { 416 if (x == null) throw new ArgumentNullException(nameof(x)); 417 if (y == null) throw new ArgumentNullException(nameof(y)); 418 419 int c = x.LifetimeEnd.CompareTo(y.LifetimeEnd); 420 return c != 0 ? c : x.ChildId.CompareTo(y.ChildId); 421 } 422 } 423 } 424}