A game framework written with osu! in mind.
at master 231 lines 8.4 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.Threading; 6using NUnit.Framework; 7using osu.Framework.Allocation; 8using osu.Framework.Graphics.Containers; 9using osu.Framework.Graphics.Sprites; 10using osu.Framework.Graphics.Textures; 11using osu.Framework.IO.Stores; 12using osu.Framework.Platform; 13using osu.Framework.Testing; 14 15namespace osu.Framework.Tests.Visual.Sprites 16{ 17 public class TestSceneTextures : FrameworkTestScene 18 { 19 [Cached] 20 private TextureStore normalStore; 21 22 [Cached] 23 private LargeTextureStore largeStore; 24 25 private BlockingOnlineStore blockingOnlineStore; 26 27 protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) 28 { 29 var host = parent.Get<GameHost>(); 30 31 blockingOnlineStore = new BlockingOnlineStore(); 32 33 normalStore = new TextureStore(host.CreateTextureLoaderStore(blockingOnlineStore)); 34 largeStore = new LargeTextureStore(host.CreateTextureLoaderStore(blockingOnlineStore)); 35 36 return base.CreateChildDependencies(parent); 37 } 38 39 [SetUpSteps] 40 public void SetUpSteps() 41 { 42 AddStep("reset online store", () => blockingOnlineStore.Reset()); 43 44 // required to drop reference counts and allow fresh lookups to occur on the LargeTextureStore. 45 AddStep("dispose children", () => Clear()); 46 } 47 48 /// <summary> 49 /// Tests that a ref-counted texture is disposed when all references are lost. 50 /// </summary> 51 [Test] 52 public void TestRefCountTextureDisposal() 53 { 54 Avatar avatar1 = null; 55 Avatar avatar2 = null; 56 Texture texture = null; 57 58 AddStep("add disposable sprite", () => avatar1 = addSprite("https://a.ppy.sh/3")); 59 AddStep("add disposable sprite", () => avatar2 = addSprite("https://a.ppy.sh/3")); 60 61 AddUntilStep("wait for texture load", () => avatar1.Texture != null && avatar2.Texture != null); 62 63 AddAssert("both textures are RefCount", () => avatar1.Texture is TextureWithRefCount && avatar2.Texture is TextureWithRefCount); 64 65 AddAssert("textures share gl texture", () => avatar1.Texture.TextureGL == avatar2.Texture.TextureGL); 66 AddAssert("textures have different refcount textures", () => avatar1.Texture != avatar2.Texture); 67 68 AddStep("dispose children", () => 69 { 70 texture = avatar1.Texture; 71 72 Clear(); 73 avatar1.Dispose(); 74 avatar2.Dispose(); 75 }); 76 77 assertAvailability(() => texture, false); 78 } 79 80 /// <summary> 81 /// Tests the case where multiple lookups occur for different textures, which shouldn't block each other. 82 /// </summary> 83 [Test] 84 public void TestFetchContentionDifferentLookup() 85 { 86 Avatar avatar1 = null; 87 Avatar avatar2 = null; 88 89 AddStep("begin blocking load", () => blockingOnlineStore.StartBlocking("https://a.ppy.sh/3")); 90 91 AddStep("get first", () => avatar1 = addSprite("https://a.ppy.sh/3")); 92 AddUntilStep("wait for first to begin loading", () => blockingOnlineStore.TotalInitiatedLookups == 1); 93 94 AddStep("get second", () => avatar2 = addSprite("https://a.ppy.sh/2")); 95 96 AddUntilStep("wait for avatar2 load", () => avatar2.Texture != null); 97 98 AddAssert("avatar1 not loaded", () => avatar1.Texture == null); 99 AddAssert("only one lookup occurred", () => blockingOnlineStore.TotalCompletedLookups == 1); 100 101 AddStep("unblock load", () => blockingOnlineStore.AllowLoad()); 102 103 AddUntilStep("wait for texture load", () => avatar1.Texture != null); 104 AddAssert("two lookups occurred", () => blockingOnlineStore.TotalCompletedLookups == 2); 105 } 106 107 /// <summary> 108 /// Tests the case where multiple lookups occur which overlap each other, for the same texture. 109 /// </summary> 110 [Test] 111 public void TestFetchContentionSameLookup() 112 { 113 Avatar avatar1 = null; 114 Avatar avatar2 = null; 115 116 AddStep("begin blocking load", () => blockingOnlineStore.StartBlocking()); 117 AddStep("get first", () => avatar1 = addSprite("https://a.ppy.sh/3")); 118 AddStep("get second", () => avatar2 = addSprite("https://a.ppy.sh/3")); 119 120 AddAssert("neither are loaded", () => avatar1.Texture == null && avatar2.Texture == null); 121 122 AddStep("unblock load", () => blockingOnlineStore.AllowLoad()); 123 124 AddUntilStep("wait for texture load", () => avatar1.Texture != null && avatar2.Texture != null); 125 126 AddAssert("only one lookup occurred", () => blockingOnlineStore.TotalInitiatedLookups == 1); 127 } 128 129 /// <summary> 130 /// Tests that a ref-counted texture gets put in a non-available state when disposed. 131 /// </summary> 132 [Test] 133 public void TestRefCountTextureAvailability() 134 { 135 Texture texture = null; 136 137 AddStep("get texture", () => texture = largeStore.Get("https://a.ppy.sh/3")); 138 AddStep("dispose texture", () => texture.Dispose()); 139 140 assertAvailability(() => texture, false); 141 } 142 143 /// <summary> 144 /// Tests that a non-ref-counted texture remains in an available state when disposed. 145 /// </summary> 146 [Test] 147 public void TestTextureAvailability() 148 { 149 Texture texture = null; 150 151 AddStep("get texture", () => texture = normalStore.Get("https://a.ppy.sh/3")); 152 AddStep("dispose texture", () => texture.Dispose()); 153 154 AddAssert("texture is still available", () => texture.Available); 155 } 156 157 private void assertAvailability(Func<Texture> textureFunc, bool available) 158 => AddAssert($"texture available = {available}", () => ((TextureWithRefCount)textureFunc()).IsDisposed == !available); 159 160 private Avatar addSprite(string url) 161 { 162 var avatar = new Avatar(url); 163 Add(new DelayedLoadWrapper(avatar)); 164 return avatar; 165 } 166 167 [LongRunningLoad] 168 private class Avatar : Sprite 169 { 170 private readonly string url; 171 172 public Avatar(string url) 173 { 174 this.url = url; 175 } 176 177 [BackgroundDependencyLoader] 178 private void load(LargeTextureStore textures) 179 { 180 Texture = textures.Get(url); 181 } 182 } 183 184 private class BlockingOnlineStore : OnlineStore 185 { 186 /// <summary> 187 /// The total number of lookups requested on this store (including blocked lookups). 188 /// </summary> 189 public int TotalInitiatedLookups { get; private set; } 190 191 /// <summary> 192 /// The total number of completed lookups. 193 /// </summary> 194 public int TotalCompletedLookups { get; private set; } 195 196 private readonly ManualResetEventSlim resetEvent = new ManualResetEventSlim(true); 197 198 private string blockingUrl; 199 200 /// <summary> 201 /// Block load until <see cref="AllowLoad"/> is called. 202 /// </summary> 203 /// <param name="blockingUrl">If not <c>null</c> or empty, only lookups for this particular URL will be blocked.</param> 204 public void StartBlocking(string blockingUrl = null) 205 { 206 this.blockingUrl = blockingUrl; 207 resetEvent.Reset(); 208 } 209 210 public void AllowLoad() => resetEvent.Set(); 211 212 public override byte[] Get(string url) 213 { 214 TotalInitiatedLookups++; 215 216 if (string.IsNullOrEmpty(blockingUrl) || url == blockingUrl) 217 resetEvent.Wait(); 218 219 TotalCompletedLookups++; 220 return base.Get(url); 221 } 222 223 public void Reset() 224 { 225 AllowLoad(); 226 TotalInitiatedLookups = 0; 227 TotalCompletedLookups = 0; 228 } 229 } 230 } 231}