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.Graphics.OpenGL;
6using osu.Framework.Graphics.OpenGL.Textures;
7using osu.Framework.IO.Stores;
8using System.Collections.Generic;
9using System.Diagnostics;
10using System.Threading.Tasks;
11using JetBrains.Annotations;
12using osu.Framework.Logging;
13using osuTK.Graphics.ES30;
14
15namespace osu.Framework.Graphics.Textures
16{
17 public class TextureStore : ResourceStore<TextureUpload>
18 {
19 private readonly Dictionary<string, Texture> textureCache = new Dictionary<string, Texture>();
20
21 private readonly All filteringMode;
22 private readonly bool manualMipmaps;
23
24 protected TextureAtlas Atlas;
25
26 private const int max_atlas_size = 1024;
27
28 /// <summary>
29 /// Decides at what resolution multiple this <see cref="TextureStore"/> is providing sprites at.
30 /// ie. if we are providing high resolution (at 2x the resolution of standard 1366x768) sprites this should be 2.
31 /// </summary>
32 public readonly float ScaleAdjust;
33
34 public TextureStore(IResourceStore<TextureUpload> store = null, bool useAtlas = true, All filteringMode = All.Linear, bool manualMipmaps = false, float scaleAdjust = 2)
35 : base(store)
36 {
37 this.filteringMode = filteringMode;
38 this.manualMipmaps = manualMipmaps;
39
40 ScaleAdjust = scaleAdjust;
41
42 AddExtension(@"png");
43 AddExtension(@"jpg");
44
45 if (useAtlas)
46 {
47 int size = Math.Min(max_atlas_size, GLWrapper.MaxTextureSize);
48 Atlas = new TextureAtlas(size, size, filteringMode: filteringMode, manualMipmaps: manualMipmaps);
49 }
50 }
51
52 private async Task<Texture> getTextureAsync(string name, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None) => loadRaw(await base.GetAsync(name).ConfigureAwait(false), wrapModeS, wrapModeT);
53
54 private Texture getTexture(string name, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None) => loadRaw(base.Get(name), wrapModeS, wrapModeT);
55
56 private Texture loadRaw(TextureUpload upload, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None)
57 {
58 if (upload == null) return null;
59
60 TextureGL glTexture = null;
61
62 if (Atlas != null)
63 {
64 if ((glTexture = Atlas.Add(upload.Width, upload.Height, wrapModeS, wrapModeT)) == null)
65 {
66 Logger.Log(
67 $"Texture requested ({upload.Width}x{upload.Height}) which exceeds {nameof(TextureStore)}'s atlas size ({max_atlas_size}x{max_atlas_size}) - bypassing atlasing. Consider using {nameof(LargeTextureStore)}.",
68 LoggingTarget.Performance);
69 }
70 }
71
72 glTexture ??= new TextureGLSingle(upload.Width, upload.Height, manualMipmaps, filteringMode, wrapModeS, wrapModeT);
73
74 Texture tex = new Texture(glTexture) { ScaleAdjust = ScaleAdjust };
75 tex.SetData(upload);
76
77 return tex;
78 }
79
80 /// <summary>
81 /// Retrieves a texture from the store and adds it to the atlas.
82 /// </summary>
83 /// <param name="name">The name of the texture.</param>
84 /// <returns>The texture.</returns>
85 public new Task<Texture> GetAsync(string name) => GetAsync(name, default, default);
86
87 /// <summary>
88 /// Retrieves a texture from the store and adds it to the atlas.
89 /// </summary>
90 /// <param name="name">The name of the texture.</param>
91 /// <param name="wrapModeS">The texture wrap mode in horizontal direction.</param>
92 /// <param name="wrapModeT">The texture wrap mode in vertical direction.</param>
93 /// <returns>The texture.</returns>
94 public Task<Texture> GetAsync(string name, WrapMode wrapModeT, WrapMode wrapModeS) =>
95 Task.Run(() => Get(name, wrapModeS, wrapModeT)); // TODO: best effort. need to re-think textureCache data structure to fix this.
96
97 /// <summary>
98 /// Retrieves a texture from the store and adds it to the atlas.
99 /// </summary>
100 /// <param name="name">The name of the texture.</param>
101 /// <returns>The texture.</returns>
102 public new Texture Get(string name) => Get(name, default, default);
103
104 private readonly Dictionary<string, Task> retrievalCompletionSources = new Dictionary<string, Task>();
105
106 /// <summary>
107 /// Retrieves a texture from the store and adds it to the atlas.
108 /// </summary>
109 /// <param name="name">The name of the texture.</param>
110 /// <param name="wrapModeS">The texture wrap mode in horizontal direction.</param>
111 /// <param name="wrapModeT">The texture wrap mode in vertical direction.</param>
112 /// <returns>The texture.</returns>
113 public virtual Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT)
114 {
115 if (string.IsNullOrEmpty(name)) return null;
116
117 string key = $"{name}:wrap-{(int)wrapModeS}-{(int)wrapModeT}";
118
119 TaskCompletionSource<Texture> tcs = null;
120 Task task;
121
122 lock (retrievalCompletionSources)
123 {
124 // Check if the texture exists in the cache.
125 if (TryGetCached(key, out var cached))
126 return cached;
127
128 // check if an existing lookup was already started for this key.
129 if (!retrievalCompletionSources.TryGetValue(key, out task))
130 // if not, take responsibility for the lookup.
131 retrievalCompletionSources[key] = (tcs = new TaskCompletionSource<Texture>()).Task;
132 }
133
134 // handle the case where a lookup is already in progress.
135 if (task != null)
136 {
137 task.Wait();
138
139 // always perform re-lookups through TryGetCached (see LargeTextureStore which has a custom implementation of this where it matters).
140 if (TryGetCached(key, out var cached))
141 return cached;
142
143 return null;
144 }
145
146 this.LogIfNonBackgroundThread(key);
147
148 Texture tex = null;
149
150 try
151 {
152 tex = getTexture(name, wrapModeS, wrapModeT);
153 if (tex != null)
154 tex.LookupKey = key;
155
156 return CacheAndReturnTexture(key, tex);
157 }
158 catch (TextureTooLargeForGLException)
159 {
160 Logger.Log($"Texture \"{name}\" exceeds the maximum size supported by this device ({GLWrapper.MaxTextureSize}px).", level: LogLevel.Error);
161 }
162 finally
163 {
164 // notify other lookups waiting on the same name lookup.
165 lock (retrievalCompletionSources)
166 {
167 Debug.Assert(tcs != null);
168
169 tcs.SetResult(tex);
170 retrievalCompletionSources.Remove(key);
171 }
172 }
173
174 return null;
175 }
176
177 /// <summary>
178 /// Attempts to retrieve an existing cached texture.
179 /// </summary>
180 /// <param name="lookupKey">The lookup key that uniquely identifies textures in the cache.</param>
181 /// <param name="texture">The returned texture. Null if the texture did not exist in the cache.</param>
182 /// <returns>Whether a cached texture was retrieved.</returns>
183 protected virtual bool TryGetCached([NotNull] string lookupKey, [CanBeNull] out Texture texture)
184 {
185 lock (textureCache)
186 return textureCache.TryGetValue(lookupKey, out texture);
187 }
188
189 /// <summary>
190 /// Caches and returns the given texture.
191 /// </summary>
192 /// <param name="lookupKey">The lookup key that uniquely identifies textures in the cache.</param>
193 /// <param name="texture">The texture to be cached and returned.</param>
194 /// <returns>The texture to be returned.</returns>
195 [CanBeNull]
196 protected virtual Texture CacheAndReturnTexture([NotNull] string lookupKey, [CanBeNull] Texture texture)
197 {
198 lock (textureCache)
199 return textureCache[lookupKey] = texture;
200 }
201
202 /// <summary>
203 /// Disposes and removes a texture from the cache.
204 /// </summary>
205 /// <param name="texture">The texture to purge from the cache.</param>
206 protected void Purge(Texture texture)
207 {
208 lock (textureCache)
209 {
210 if (textureCache.TryGetValue(texture.LookupKey, out var tex))
211 {
212 // we are doing this locally as right now, Textures don't dispose the underlying texture (leaving it to GC finalizers).
213 // in the case of a purge operation we are pretty sure this is the intended behaviour.
214 tex?.TextureGL?.Dispose();
215 tex?.Dispose();
216 }
217
218 textureCache.Remove(texture.LookupKey);
219 }
220 }
221 }
222}