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 System.Collections.Generic;
6using System.Diagnostics;
7using System.Threading;
8using osu.Framework.Allocation;
9using osu.Framework.Extensions.TypeExtensions;
10using osu.Framework.Graphics.Containers;
11using osu.Framework.Statistics;
12
13namespace osu.Framework.Graphics.Pooling
14{
15 /// <summary>
16 /// A component which provides a pool of reusable drawables.
17 /// Should be used to reduce allocation and construction overhead of individual drawables.
18 /// </summary>
19 /// <remarks>
20 /// The <see cref="initialSize"/> drawables will be prepared ahead-of-time during this pool's asynchronous load procedure.
21 /// Drawables exceeding the pool's available size will not be asynchronously loaded as it is assumed they are immediately required for consumption.
22 /// </remarks>
23 /// <typeparam name="T">The type of drawable to be pooled.</typeparam>
24 public class DrawablePool<T> : CompositeDrawable, IDrawablePool where T : PoolableDrawable, new()
25 {
26 private GlobalStatistic<DrawablePoolUsageStatistic> statistic;
27
28 private readonly int initialSize;
29 private readonly int? maximumSize;
30
31 private readonly Stack<T> pool = new Stack<T>();
32
33 // ReSharper disable once StaticMemberInGenericType (this is intentional, we want a separate count per type).
34 private static int poolInstanceID;
35
36 /// <summary>
37 /// Create a new pool instance.
38 /// </summary>
39 /// <param name="initialSize">The number of drawables to be prepared for initial consumption.</param>
40 /// <param name="maximumSize">An optional maximum size after which the pool will no longer be expanded.</param>
41 public DrawablePool(int initialSize, int? maximumSize = null)
42 {
43 this.maximumSize = maximumSize;
44 this.initialSize = initialSize;
45
46 int id = Interlocked.Increment(ref poolInstanceID);
47
48 statistic = GlobalStatistics.Get<DrawablePoolUsageStatistic>(nameof(DrawablePool<T>), typeof(T).ReadableName() + $"`{id}");
49 statistic.Value = new DrawablePoolUsageStatistic();
50 }
51
52 [BackgroundDependencyLoader]
53 private void load()
54 {
55 for (int i = 0; i < initialSize; i++)
56 push(create());
57
58 LoadComponents(pool.ToArray());
59 }
60
61 /// <summary>
62 /// Return a drawable after use.
63 /// </summary>
64 /// <param name="pooledDrawable">The drawable to return. Should have originally come from this pool.</param>
65 public void Return(PoolableDrawable pooledDrawable)
66 {
67 if (!(pooledDrawable is T))
68 throw new ArgumentException("Invalid type", nameof(pooledDrawable));
69
70 if (pooledDrawable.Parent != null)
71 throw new InvalidOperationException("Drawable was attempted to be returned to pool while still in a hierarchy");
72
73 if (pooledDrawable.IsInUse)
74 {
75 // if the return operation didn't come from the drawable, redirect to ensure consistent behaviour.
76 pooledDrawable.Return();
77 return;
78 }
79
80 //TODO: check the drawable was sourced from this pool for safety.
81 push((T)pooledDrawable);
82 CountInUse--;
83 }
84
85 PoolableDrawable IDrawablePool.Get(Action<PoolableDrawable> setupAction) => Get(setupAction);
86
87 /// <summary>
88 /// Get a drawable from this pool.
89 /// </summary>
90 /// <param name="setupAction">An optional action to be performed on this drawable immediately after retrieval. Should generally be used to prepare the drawable into a usable state.</param>
91 /// <returns>The drawable.</returns>
92 public T Get(Action<T> setupAction = null)
93 {
94 if (!pool.TryPop(out var drawable))
95 {
96 drawable = create();
97
98 if (LoadState >= LoadState.Loading)
99 LoadComponent(drawable);
100 }
101 else
102 CountAvailable--;
103
104 drawable.Assign();
105 drawable.LifetimeStart = double.MinValue;
106 drawable.LifetimeEnd = double.MaxValue;
107
108 setupAction?.Invoke(drawable);
109
110 CountInUse++;
111 return drawable;
112 }
113
114 /// <summary>
115 /// Create a new drawable to be consumed or added to the pool.
116 /// </summary>
117 protected virtual T CreateNewDrawable() => new T();
118
119 private T create()
120 {
121 var drawable = CreateNewDrawable();
122 drawable.SetPool(this);
123 CountConstructed++;
124
125 return drawable;
126 }
127
128 private bool push(T poolableDrawable)
129 {
130 if (CountAvailable >= maximumSize)
131 {
132 // if the drawable can't be returned to the pool, mark it as such so it can be disposed of.
133 poolableDrawable.SetPool(null);
134
135 // then attempt disposal.
136 if (poolableDrawable.DisposeOnDeathRemoval)
137 DisposeChildAsync(poolableDrawable);
138
139 return false;
140 }
141
142 pool.Push(poolableDrawable);
143 CountAvailable++;
144
145 return true;
146 }
147
148 protected override void Dispose(bool isDisposing)
149 {
150 base.Dispose(isDisposing);
151
152 foreach (var p in pool)
153 p.Dispose();
154
155 CountInUse = 0;
156 CountConstructed = 0;
157 CountAvailable = 0;
158
159 GlobalStatistics.Remove(statistic);
160
161 // Disallow any further Gets/Returns to adjust the statistics.
162 statistic = null;
163 }
164
165 private int countInUse;
166
167 /// <summary>
168 /// The number of drawables currently in use.
169 /// </summary>
170 public int CountInUse
171 {
172 get => countInUse;
173 private set
174 {
175 Debug.Assert(statistic != null);
176
177 statistic.Value.CountInUse += value - countInUse;
178 countInUse = value;
179 }
180 }
181
182 private int countConstructed;
183
184 /// <summary>
185 /// The total number of drawables constructed.
186 /// </summary>
187 public int CountConstructed
188 {
189 get => countConstructed;
190 private set
191 {
192 Debug.Assert(statistic != null);
193
194 statistic.Value.CountConstructed += value - countConstructed;
195 countConstructed = value;
196 }
197 }
198
199 private int countAvailable;
200
201 /// <summary>
202 /// The number of drawables currently available for consumption.
203 /// </summary>
204 public int CountAvailable
205 {
206 get => countAvailable;
207 private set
208 {
209 Debug.Assert(statistic != null);
210
211 statistic.Value.CountAvailable += value - countAvailable;
212 countAvailable = value;
213 }
214 }
215
216 private class DrawablePoolUsageStatistic
217 {
218 /// <summary>
219 /// Total number of drawables available for use (in the pool).
220 /// </summary>
221 public int CountAvailable;
222
223 /// <summary>
224 /// Total number of drawables currently in use.
225 /// </summary>
226 public int CountInUse;
227
228 /// <summary>
229 /// Total number of drawables constructed (can exceed the max count).
230 /// </summary>
231 public int CountConstructed;
232
233 public override string ToString() => $"{CountAvailable}/{CountConstructed} ({CountInUse})";
234 }
235 }
236}