A game framework written with osu! in mind.
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}