A game framework written with osu! in mind.

Fix LargeTextureStore race conditions

+64 -32
+26 -16
osu.Framework/Graphics/Textures/LargeTextureStore.cs
··· 4 4 using System.Collections.Generic; 5 5 using System.Diagnostics; 6 6 using System.Threading; 7 + using JetBrains.Annotations; 7 8 using osu.Framework.IO.Stores; 8 9 using osuTK.Graphics.ES30; 9 - using osu.Framework.Graphics.OpenGL.Textures; 10 10 11 11 namespace osu.Framework.Graphics.Textures 12 12 { ··· 23 23 { 24 24 } 25 25 26 - /// <summary> 27 - /// Retrieves a texture. 28 - /// This texture should only be assigned once, as reference counting is being used internally. 29 - /// If you wish to use the same texture multiple times, call this method an equal number of times. 30 - /// </summary> 31 - /// <param name="name">The name of the texture.</param> 32 - /// <param name="wrapModeS">The horizontal wrap mode of the texture.</param> 33 - /// <param name="wrapModeT">The vertical wrap mode of the texture.</param> 34 - /// <returns>The texture.</returns> 35 - public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) 26 + protected override bool TryGetCached(string lookupKey, out Texture texture) 36 27 { 37 - var tex = base.Get(name, wrapModeS, wrapModeT); 28 + lock (referenceCountLock) 29 + { 30 + if (base.TryGetCached(lookupKey, out var tex)) 31 + { 32 + texture = createTextureWithRefCount(lookupKey, tex); 33 + return true; 34 + } 38 35 39 - if (tex?.TextureGL == null) 36 + texture = null; 37 + return false; 38 + } 39 + } 40 + 41 + protected override Texture CacheAndReturnTexture(string lookupKey, Texture texture) 42 + { 43 + lock (referenceCountLock) 44 + return createTextureWithRefCount(lookupKey, base.CacheAndReturnTexture(lookupKey, texture)); 45 + } 46 + 47 + private TextureWithRefCount createTextureWithRefCount([NotNull] string lookupKey, [CanBeNull] Texture baseTexture) 48 + { 49 + if (baseTexture == null) 40 50 return null; 41 51 42 52 lock (referenceCountLock) 43 53 { 44 - if (!referenceCounts.TryGetValue(tex.LookupKey, out TextureWithRefCount.ReferenceCount count)) 45 - referenceCounts[tex.LookupKey] = count = new TextureWithRefCount.ReferenceCount(referenceCountLock, () => onAllReferencesLost(tex)); 54 + if (!referenceCounts.TryGetValue(lookupKey, out TextureWithRefCount.ReferenceCount count)) 55 + referenceCounts[lookupKey] = count = new TextureWithRefCount.ReferenceCount(referenceCountLock, () => onAllReferencesLost(baseTexture)); 46 56 47 - return new TextureWithRefCount(tex.TextureGL, count); 57 + return new TextureWithRefCount(baseTexture.TextureGL, count); 48 58 } 49 59 } 50 60
+38 -16
osu.Framework/Graphics/Textures/TextureStore.cs
··· 7 7 using osu.Framework.IO.Stores; 8 8 using System.Collections.Generic; 9 9 using System.Threading.Tasks; 10 + using JetBrains.Annotations; 10 11 using osu.Framework.Logging; 11 12 using osuTK.Graphics.ES30; 12 13 ··· 61 62 if (Atlas != null) 62 63 { 63 64 if ((glTexture = Atlas.Add(upload.Width, upload.Height, wrapModeS, wrapModeT)) == null) 64 - Logger.Log($"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)}.", LoggingTarget.Performance); 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 + } 65 70 } 66 71 67 72 glTexture ??= new TextureGLSingle(upload.Width, upload.Height, manualMipmaps, filteringMode, wrapModeS, wrapModeT); ··· 86 91 /// <param name="wrapModeS">The texture wrap mode in horizontal direction.</param> 87 92 /// <param name="wrapModeT">The texture wrap mode in vertical direction.</param> 88 93 /// <returns>The texture.</returns> 89 - public Task<Texture> GetAsync(string name, WrapMode wrapModeT, WrapMode wrapModeS) => Task.Run(() => Get(name, wrapModeS, wrapModeT)); // TODO: best effort. need to re-think textureCache data structure to fix this. 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. 90 96 91 97 /// <summary> 92 98 /// Retrieves a texture from the store and adds it to the atlas. ··· 111 117 this.LogIfNonBackgroundThread(key); 112 118 113 119 // Check if the texture exists in the cache. 114 - lock (textureCache) 115 - { 116 - if (textureCache.TryGetValue(key, out var tex)) 117 - return tex; 118 - } 120 + if (TryGetCached(key, out var cached)) 121 + return cached; 119 122 120 123 // Take an exclusive lock on retrieval of the texture. 121 124 lock (retrievalLock) 122 125 { 123 126 // If another retrieval of the texture happened before us, we should check if the texture exists in the cache again. 124 - lock (textureCache) 125 - { 126 - if (textureCache.TryGetValue(key, out var tex)) 127 - return tex; 128 - } 127 + if (TryGetCached(key, out cached)) 128 + return cached; 129 129 130 130 try 131 131 { ··· 133 133 if (tex != null) 134 134 tex.LookupKey = key; 135 135 136 - lock (textureCache) 137 - textureCache[key] = tex; 138 - 139 - return tex; 136 + return CacheAndReturnTexture(key, tex); 140 137 } 141 138 catch (TextureTooLargeForGLException) 142 139 { ··· 145 142 } 146 143 147 144 return null; 145 + } 146 + 147 + /// <summary> 148 + /// Attempts to retrieve an existing cached texture. 149 + /// </summary> 150 + /// <param name="lookupKey">The lookup key that uniquely identifies textures in the cache.</param> 151 + /// <param name="texture">The returned texture. Null if the texture did not exist in the cache.</param> 152 + /// <returns>Whether a cached texture was retrieved.</returns> 153 + protected virtual bool TryGetCached([NotNull] string lookupKey, [CanBeNull] out Texture texture) 154 + { 155 + lock (textureCache) 156 + return textureCache.TryGetValue(lookupKey, out texture); 157 + } 158 + 159 + /// <summary> 160 + /// Caches and returns the given texture. 161 + /// </summary> 162 + /// <param name="lookupKey">The lookup key that uniquely identifies textures in the cache.</param> 163 + /// <param name="texture">The texture to be cached and returned.</param> 164 + /// <returns>The texture to be returned.</returns> 165 + [CanBeNull] 166 + protected virtual Texture CacheAndReturnTexture([NotNull] string lookupKey, [CanBeNull] Texture texture) 167 + { 168 + lock (textureCache) 169 + return textureCache[lookupKey] = texture; 148 170 } 149 171 150 172 /// <summary>