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.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}