A game framework written with osu! in mind.
at master 142 lines 5.7 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 osu.Framework.Statistics; 6using System.Diagnostics; 7using osu.Framework.Layout; 8using osu.Framework.Threading; 9using osu.Framework.Timing; 10 11namespace osu.Framework.Graphics.Containers 12{ 13 public class DelayedLoadUnloadWrapper : DelayedLoadWrapper 14 { 15 private readonly double timeBeforeUnload; 16 17 public DelayedLoadUnloadWrapper(Func<Drawable> createContentFunction, double timeBeforeLoad = 500, double timeBeforeUnload = 1000) 18 : base(createContentFunction, timeBeforeLoad) 19 { 20 this.timeBeforeUnload = timeBeforeUnload; 21 22 AddLayout(unloadClockBacking); 23 } 24 25 private static readonly GlobalStatistic<int> total_loaded = GlobalStatistics.Get<int>("Drawable", $"{nameof(DelayedLoadUnloadWrapper)}s"); 26 27 private double timeHidden; 28 29 private ScheduledDelegate unloadSchedule; 30 31 protected bool ShouldUnloadContent => timeBeforeUnload == 0 || timeHidden > timeBeforeUnload; 32 33 private ScheduledDelegate scheduledUnloadCheckRegistration; 34 35 protected override void EndDelayedLoad(Drawable content) 36 { 37 base.EndDelayedLoad(content); 38 39 // Scheduled for another frame since Update() may not have run yet and thus OptimisingContainer may not be up-to-date 40 scheduledUnloadCheckRegistration = Game.Schedule(() => 41 { 42 // Since this code is running on the game scheduler, it needs to be safe against a potential simultaneous async disposal. 43 lock (disposalLock) 44 { 45 if (isDisposed) 46 return; 47 48 // Content must have finished loading, but not necessarily added to the hierarchy. 49 Debug.Assert(DelayedLoadTriggered); 50 Debug.Assert(Content.LoadState >= LoadState.Ready); 51 52 Debug.Assert(unloadSchedule == null); 53 unloadSchedule = Game.Scheduler.AddDelayed(checkForUnload, 0, true); 54 Debug.Assert(unloadSchedule != null); 55 56 total_loaded.Value++; 57 } 58 }); 59 } 60 61 private readonly object disposalLock = new object(); 62 private bool isDisposed; 63 64 protected override void Dispose(bool isDisposing) 65 { 66 lock (disposalLock) 67 isDisposed = true; 68 69 base.Dispose(isDisposing); 70 } 71 72 protected override void CancelTasks() 73 { 74 base.CancelTasks(); 75 76 if (unloadSchedule != null) 77 { 78 unloadSchedule.Cancel(); 79 unloadSchedule = null; 80 81 total_loaded.Value--; 82 } 83 84 scheduledUnloadCheckRegistration?.Cancel(); 85 scheduledUnloadCheckRegistration = null; 86 } 87 88 private readonly LayoutValue<IFrameBasedClock> unloadClockBacking = new LayoutValue<IFrameBasedClock>(Invalidation.Parent); 89 90 private IFrameBasedClock unloadClock => unloadClockBacking.IsValid ? unloadClockBacking.Value : (unloadClockBacking.Value = FindClosestParent<Game>() == null ? Game.Clock : Clock); 91 92 private void checkForUnload() 93 { 94 // Since this code is running on the game scheduler, it needs to be safe against a potential simultaneous async disposal. 95 lock (disposalLock) 96 { 97 if (isDisposed) 98 return; 99 100 // Guard against multiple executions of checkForUnload() without an intermediate load having started. 101 Debug.Assert(DelayedLoadTriggered); 102 Debug.Assert(Content.LoadState >= LoadState.Ready); 103 104 // This code can be expensive, so only run if we haven't yet loaded. 105 if (IsIntersecting) 106 timeHidden = 0; 107 else 108 timeHidden += unloadClock.ElapsedFrameTime; 109 110 // Don't unload if we don't need to. 111 if (!ShouldUnloadContent) 112 return; 113 114 // We need to dispose the content, taking into account what we know at this point in time: 115 // 1: The wrapper has not been disposed. Consequently, neither has the content. 116 // 2: The content has finished loading. 117 // 3: The content may not have been added to the hierarchy (e.g. if this wrapper is hidden). This is dependent upon the value of DelayedLoadCompleted. 118 if (DelayedLoadCompleted) 119 { 120 Debug.Assert(Content.LoadState >= LoadState.Ready); 121 ClearInternal(); // Content added, remove AND dispose. 122 } 123 else 124 { 125 Debug.Assert(Content.LoadState == LoadState.Ready); 126 DisposeChildAsync(Content); // Content not added, only need to dispose. 127 } 128 129 Content = null; 130 timeHidden = 0; 131 132 // This has two important roles: 133 // 1. Stopping this delegate from executing multiple times. 134 // 2. If DelayedLoadCompleted = false (content not yet added to hierarchy), prevents the now disposed content from being added (e.g. if this wrapper becomes visible again). 135 CancelTasks(); 136 137 // And finally, allow another load to take place. 138 DelayedLoadTriggered = DelayedLoadCompleted = false; 139 } 140 } 141 } 142}