using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Metadata;
using StardewValley;
namespace StardewModdingAPI.Framework
{
/// A thread-safe content manager which intercepts assets being loaded to let SMAPI mods inject or edit them.
///
/// This is the centralised content manager which manages all game assets. The game and mods don't use this class
/// directly; instead they use one of several instances, which proxy requests to
/// this class. That ensures that when the game disposes one content manager, the others can continue unaffected.
/// That notably requires this class to be thread-safe, since the content managers can be disposed asynchronously.
///
/// Note that assets in the cache have two identifiers: the asset name (like "bundles") and key (like "bundles.pt-BR").
/// For English and non-translatable assets, these have the same value. The underlying cache only knows about asset
/// keys, and the game and mods only know about asset names. The content manager handles resolving them.
///
internal class SContentManager : LocalizedContentManager
{
/*********
** Properties
*********/
/// The preferred directory separator chaeacter in an asset key.
private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString();
/// Encapsulates monitoring and logging.
private readonly IMonitor Monitor;
/// The underlying asset cache.
private readonly ContentCache Cache;
/// The private method which generates the locale portion of an asset name.
private readonly IPrivateMethod GetKeyLocale;
/// The language codes used in asset keys.
private readonly IDictionary KeyLocales;
/// Provides metadata for core game assets.
private readonly CoreAssets CoreAssets;
/// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset.
private readonly ContextHash AssetsBeingLoaded = new ContextHash();
/// A lookup of the content managers which loaded each asset.
private readonly IDictionary> ContentManagersByAssetKey = new Dictionary>();
/// The path prefix for assets in mod folders.
private readonly string ModContentPrefix;
/// A lock used to prevents concurrent changes to the cache while data is being read.
private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
/*********
** Accessors
*********/
/// Interceptors which provide the initial versions of matching assets.
internal IDictionary> Loaders { get; } = new Dictionary>();
/// Interceptors which edit matching assets after they're loaded.
internal IDictionary> Editors { get; } = new Dictionary>();
/// The possible directory separator characters in an asset key.
internal static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray();
/// The absolute path to the .
internal string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory);
/*********
** Public methods
*********/
/****
** Constructor
****/
/// Construct an instance.
/// The service provider to use to locate services.
/// The root directory to search for content.
/// The current culture for which to localise content.
/// The current language code for which to localise content.
/// Encapsulates monitoring and logging.
/// Simplifies access to private code.
public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection)
: base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride)
{
// init
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator);
this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode");
this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath);
// get asset data
this.CoreAssets = new CoreAssets(this.NormaliseAssetName);
this.KeyLocales = this.GetKeyLocales(reflection);
}
/****
** Asset key/name handling
****/
/// Normalise path separators in a file path. For asset keys, see instead.
/// The file path to normalise.
[Pure]
public string NormalisePathSeparators(string path)
{
return this.Cache.NormalisePathSeparators(path);
}
/// Normalise an asset name so it's consistent with the underlying cache.
/// The asset key.
[Pure]
public string NormaliseAssetName(string assetName)
{
return this.Cache.NormaliseKey(assetName);
}
/// Assert that the given key has a valid format.
/// The asset key to check.
/// The asset key is empty or contains invalid characters.
[SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
public void AssertValidAssetKeyFormat(string key)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException("The asset key or local path is empty.");
if (key.Intersect(Path.GetInvalidPathChars()).Any())
throw new ArgumentException("The asset key or local path contains invalid characters.");
}
/// Convert an absolute file path into a appropriate asset name.
/// The absolute path to the file.
public string GetAssetNameFromFilePath(string absolutePath)
{
#if SMAPI_FOR_WINDOWS
// XNA doesn't allow absolute asset paths, so get a path relative to the content folder
return this.GetRelativePath(absolutePath);
#else
// MonoGame is weird about relative paths on Mac, but allows absolute paths
return absolutePath;
#endif
}
/****
** Content loading
****/
/// Get the current content locale.
public string GetLocale()
{
return this.GetKeyLocale.Invoke();
}
/// Get whether the content manager has already loaded and cached the given asset.
/// The asset path relative to the loader root directory, not including the .xnb extension.
public bool IsLoaded(string assetName)
{
assetName = this.Cache.NormaliseKey(assetName);
return this.WithReadLock(() => this.IsNormalisedKeyLoaded(assetName));
}
/// Get the cached asset keys.
public IEnumerable GetAssetKeys()
{
return this.WithReadLock(() =>
this.Cache.Keys
.Select(this.GetAssetName)
.Distinct()
);
}
/// Load an asset through the content pipeline. When loading a .png file, this must be called outside the game's draw loop.
/// The expected asset type.
/// The asset path relative to the content directory.
public override T Load(string assetName)
{
return this.LoadFor(assetName, this);
}
/// Load an asset through the content pipeline. When loading a .png file, this must be called outside the game's draw loop.
/// The expected asset type.
/// The asset path relative to the content directory.
/// The content manager instance for which to load the asset.
/// The is empty or contains invalid characters.
/// The content asset couldn't be loaded (e.g. because it doesn't exist).
public T LoadFor(string assetName, ContentManager instance)
{
// normalise asset key
this.AssertValidAssetKeyFormat(assetName);
assetName = this.NormaliseAssetName(assetName);
// load game content
if (!assetName.StartsWith(this.ModContentPrefix))
return this.LoadImpl(assetName, instance);
// load mod content
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}.");
try
{
return this.WithWriteLock(() =>
{
// try cache
if (this.IsLoaded(assetName))
return this.LoadImpl(assetName, instance);
// get file
FileInfo file = this.GetModFile(assetName);
if (!file.Exists)
throw GetContentError("the specified path doesn't exist.");
// load content
switch (file.Extension.ToLower())
{
// XNB file
case ".xnb":
return this.LoadImpl(assetName, instance);
// unpacked map
case ".tbin":
throw GetContentError($"can't read unpacked map file '{assetName}' directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper.");
// unpacked image
case ".png":
// validate
if (typeof(T) != typeof(Texture2D))
throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
// fetch & cache
using (FileStream stream = File.OpenRead(file.FullName))
{
Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
texture = this.PremultiplyTransparency(texture);
this.InjectWithoutLock(assetName, texture, instance);
return (T)(object)texture;
}
default:
throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'.");
}
});
}
catch (Exception ex) when (!(ex is SContentLoadException))
{
throw new SContentLoadException($"The content manager failed loading content asset '{assetName}'.", ex);
}
}
/// Inject an asset into the cache.
/// The type of asset to inject.
/// The asset path relative to the loader root directory, not including the .xnb extension.
/// The asset value.
/// The content manager instance for which to load the asset.
public void Inject(string assetName, T value, ContentManager instance)
{
this.WithWriteLock(() => this.InjectWithoutLock(assetName, value, instance));
}
/****
** Cache invalidation
****/
/// Purge assets from the cache that match one of the interceptors.
/// The asset editors for which to purge matching assets.
/// The asset loaders for which to purge matching assets.
/// Returns whether any cache entries were invalidated.
public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders)
{
if (!editors.Any() && !loaders.Any())
return false;
// get CanEdit/Load methods
MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit));
MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad));
if (canEdit == null || canLoad == null)
throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen
// invalidate matching keys
return this.InvalidateCache(asset =>
{
// check loaders
MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType);
if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { asset })))
return true;
// check editors
MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType);
return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { asset }));
});
}
/// Purge matched assets from the cache.
/// Matches the asset keys to invalidate.
/// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game.
/// Returns whether any cache entries were invalidated.
public bool InvalidateCache(Func predicate, bool dispose = false)
{
string locale = this.GetLocale();
return this.InvalidateCache((assetName, type) =>
{
IAssetInfo info = new AssetInfo(locale, assetName, type, this.NormaliseAssetName);
return predicate(info);
});
}
/// Purge matched assets from the cache.
/// Matches the asset keys to invalidate.
/// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game.
/// Returns whether any cache entries were invalidated.
public bool InvalidateCache(Func predicate, bool dispose = false)
{
return this.WithWriteLock(() =>
{
// invalidate matching keys
HashSet removeKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase);
HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase);
this.Cache.Remove((key, type) =>
{
this.ParseCacheKey(key, out string assetName, out _);
if (removeAssetNames.Contains(assetName) || predicate(assetName, type))
{
removeAssetNames.Add(assetName);
removeKeys.Add(key);
return true;
}
return false;
});
// update reference tracking
foreach (string key in removeKeys)
this.ContentManagersByAssetKey.Remove(key);
// reload core game assets
int reloaded = 0;
foreach (string key in removeAssetNames)
{
if (this.CoreAssets.ReloadForKey(this, key))
reloaded++;
}
// report result
if (removeKeys.Any())
{
this.Monitor.Log($"Invalidated {removeAssetNames.Count} asset names: {string.Join(", ", removeKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace);
return true;
}
this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace);
return false;
});
}
/****
** Disposal
****/
/// Dispose assets for the given content manager shim.
/// The content manager whose assets to dispose.
internal void DisposeFor(ContentManagerShim shim)
{
this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace);
this.WithWriteLock(() =>
{
foreach (var entry in this.ContentManagersByAssetKey)
entry.Value.Remove(shim);
this.InvalidateCache((key, type) => !this.ContentManagersByAssetKey[key].Any(), dispose: true);
});
}
/*********
** Private methods
*********/
/****
** Disposal
****/
/// Dispose held resources.
/// Whether the content manager is disposing (rather than finalising).
protected override void Dispose(bool disposing)
{
this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace);
base.Dispose(disposing);
}
/****
** Asset name/key handling
****/
/// Get a directory or file path relative to the content root.
/// The target file path.
private string GetRelativePath(string targetPath)
{
// convert to URIs
Uri from = new Uri(this.FullRootDirectory + "/");
Uri to = new Uri(targetPath + "/");
if (from.Scheme != to.Scheme)
throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{this.FullRootDirectory}'.");
// get relative path
return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())
.Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform
}
/// Get the locale codes (like ja-JP) used in asset keys.
/// Simplifies access to private game code.
private IDictionary GetKeyLocales(Reflector reflection)
{
// get the private code field directly to avoid changed-code logic
IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode");
// remember previous settings
LanguageCode previousCode = codeField.GetValue();
string previousOverride = this.LanguageCodeOverride;
// create locale => code map
IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
this.LanguageCodeOverride = null;
foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode)))
{
codeField.SetValue(code);
map[this.GetKeyLocale.Invoke()] = code;
}
// restore previous settings
codeField.SetValue(previousCode);
this.LanguageCodeOverride = previousOverride;
return map;
}
/// Get the asset name from a cache key.
/// The input cache key.
private string GetAssetName(string cacheKey)
{
this.ParseCacheKey(cacheKey, out string assetName, out string _);
return assetName;
}
/// Parse a cache key into its component parts.
/// The input cache key.
/// The original asset name.
/// The asset locale code (or null if not localised).
private void ParseCacheKey(string cacheKey, out string assetName, out string localeCode)
{
// handle localised key
if (!string.IsNullOrWhiteSpace(cacheKey))
{
int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture);
if (lastSepIndex >= 0)
{
string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1);
if (this.KeyLocales.ContainsKey(suffix))
{
assetName = cacheKey.Substring(0, lastSepIndex);
localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1);
return;
}
}
}
// handle simple key
assetName = cacheKey;
localeCode = null;
}
/****
** Cache handling
****/
/// Get whether an asset has already been loaded.
/// The normalised asset name.
private bool IsNormalisedKeyLoaded(string normalisedAssetName)
{
return this.Cache.ContainsKey(normalisedAssetName)
|| this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset
}
/// Track that a content manager loaded an asset.
/// The asset key that was loaded.
/// The content manager that loaded the asset.
private void TrackAssetLoader(string key, ContentManager manager)
{
if (!this.ContentManagersByAssetKey.TryGetValue(key, out HashSet hash))
hash = this.ContentManagersByAssetKey[key] = new HashSet();
hash.Add(manager);
}
/****
** Content loading
****/
/// Load an asset name without heuristics to support mod content.
/// The type of asset to load.
/// The asset path relative to the loader root directory, not including the .xnb extension.
/// The content manager instance for which to load the asset.
private T LoadImpl(string assetName, ContentManager instance)
{
return this.WithWriteLock(() =>
{
// skip if already loaded
if (this.IsNormalisedKeyLoaded(assetName))
{
this.TrackAssetLoader(assetName, instance);
return base.Load(assetName);
}
// load asset
T data;
if (this.AssetsBeingLoaded.Contains(assetName))
{
this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn);
this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace);
data = base.Load(assetName);
}
else
{
data = this.AssetsBeingLoaded.Track(assetName, () =>
{
IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName);
IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName);
asset = this.ApplyEditors(info, asset);
return (T)asset.Data;
});
}
// update cache & return data
this.InjectWithoutLock(assetName, data, instance);
return data;
});
}
/// Inject an asset into the cache without acquiring a write lock. This should only be called from within a write lock.
/// The type of asset to inject.
/// The asset path relative to the loader root directory, not including the .xnb extension.
/// The asset value.
/// The content manager instance for which to load the asset.
private void InjectWithoutLock(string assetName, T value, ContentManager instance)
{
assetName = this.NormaliseAssetName(assetName);
this.Cache[assetName] = value;
this.TrackAssetLoader(assetName, instance);
}
/// Get a file from the mod folder.
/// The asset path relative to the content folder.
private FileInfo GetModFile(string path)
{
// try exact match
FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path));
// try with default extension
if (!file.Exists && file.Extension.ToLower() != ".xnb")
{
FileInfo result = new FileInfo(file.FullName + ".xnb");
if (result.Exists)
file = result;
}
return file;
}
/// Load the initial asset from the registered .
/// The basic asset metadata.
/// Returns the loaded asset metadata, or null if no loader matched.
private IAssetData ApplyLoader(IAssetInfo info)
{
// find matching loaders
var loaders = this.GetInterceptors(this.Loaders)
.Where(entry =>
{
try
{
return entry.Value.CanLoad(info);
}
catch (Exception ex)
{
this.Monitor.Log($"{entry.Key.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
return false;
}
})
.ToArray();
// validate loaders
if (!loaders.Any())
return null;
if (loaders.Length > 1)
{
string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray();
this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn);
return null;
}
// fetch asset from loader
IModMetadata mod = loaders[0].Key;
IAssetLoader loader = loaders[0].Value;
T data;
try
{
data = loader.Load(info);
this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace);
}
catch (Exception ex)
{
this.Monitor.Log($"{mod.DisplayName} crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
return null;
}
// validate asset
if (data == null)
{
this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error);
return null;
}
// return matched asset
return new AssetDataForObject(info, data, this.NormaliseAssetName);
}
/// Apply any to a loaded asset.
/// The asset type.
/// The basic asset metadata.
/// The loaded asset.
private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset)
{
IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName);
// edit asset
foreach (var entry in this.GetInterceptors(this.Editors))
{
// check for match
IModMetadata mod = entry.Key;
IAssetEditor editor = entry.Value;
try
{
if (!editor.CanEdit(info))
continue;
}
catch (Exception ex)
{
this.Monitor.Log($"{mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
}
// try edit
object prevAsset = asset.Data;
try
{
editor.Edit(asset);
this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace);
}
catch (Exception ex)
{
this.Monitor.Log($"{mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
// validate edit
if (asset.Data == null)
{
this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn);
asset = GetNewData(prevAsset);
}
else if (!(asset.Data is T))
{
this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn);
asset = GetNewData(prevAsset);
}
}
// return result
return asset;
}
/// Get all registered interceptors from a list.
private IEnumerable> GetInterceptors(IDictionary> entries)
{
foreach (var entry in entries)
{
IModMetadata mod = entry.Key;
IList interceptors = entry.Value;
// registered editors
foreach (T interceptor in interceptors)
yield return new KeyValuePair(mod, interceptor);
}
}
/// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing.
/// The texture to premultiply.
/// Returns a premultiplied texture.
/// Based on code by Layoric.
private Texture2D PremultiplyTransparency(Texture2D texture)
{
// validate
if (Context.IsInDrawLoop)
throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop.");
// process texture
SpriteBatch spriteBatch = Game1.spriteBatch;
GraphicsDevice gpu = Game1.graphics.GraphicsDevice;
using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height))
{
// create blank render target to premultiply
gpu.SetRenderTarget(renderTarget);
gpu.Clear(Color.Black);
// multiply each color by the source alpha, and write just the color values into the final texture
spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState
{
ColorDestinationBlend = Blend.Zero,
ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue,
AlphaDestinationBlend = Blend.Zero,
AlphaSourceBlend = Blend.SourceAlpha,
ColorSourceBlend = Blend.SourceAlpha
});
spriteBatch.Draw(texture, texture.Bounds, Color.White);
spriteBatch.End();
// copy the alpha values from the source texture into the final one without multiplying them
spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState
{
ColorWriteChannels = ColorWriteChannels.Alpha,
AlphaDestinationBlend = Blend.Zero,
ColorDestinationBlend = Blend.Zero,
AlphaSourceBlend = Blend.One,
ColorSourceBlend = Blend.One
});
spriteBatch.Draw(texture, texture.Bounds, Color.White);
spriteBatch.End();
// release GPU
gpu.SetRenderTarget(null);
// extract premultiplied data
Color[] data = new Color[texture.Width * texture.Height];
renderTarget.GetData(data);
// unset texture from GPU to regain control
gpu.Textures[0] = null;
// update texture with premultiplied data
texture.SetData(data);
}
return texture;
}
/****
** Concurrency logic
****/
/// Acquire a read lock which prevents concurrent writes to the cache while it's open.
/// The action's return value.
/// The action to perform.
private T WithReadLock(Func action)
{
try
{
this.Lock.EnterReadLock();
return action();
}
finally
{
this.Lock.ExitReadLock();
}
}
/// Acquire a write lock which prevents concurrent reads or writes to the cache while it's open.
/// The action to perform.
private void WithWriteLock(Action action)
{
try
{
this.Lock.EnterWriteLock();
action();
}
finally
{
this.Lock.ExitWriteLock();
}
}
/// Acquire a write lock which prevents concurrent reads or writes to the cache while it's open.
/// The action's return value.
/// The action to perform.
private T WithWriteLock(Func action)
{
try
{
this.Lock.EnterReadLock();
return action();
}
finally
{
this.Lock.ExitReadLock();
}
}
}
}