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