A game framework written with osu! in mind.
at master 236 lines 7.9 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 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}