From ff94a8149ed5a0f597500bfb2b1896bdb2f1fff3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 12 Dec 2019 23:46:32 -0500 Subject: fix assets not being disposed when a content manager is disposed --- src/SMAPI/Framework/ContentManagers/BaseContentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 5283340e..93fd729b 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -200,7 +200,7 @@ namespace StardewModdingAPI.Framework.ContentManagers return true; } return false; - }); + }, dispose); return removeAssetNames.Select(p => Tuple.Create(p.Key, p.Value)); } -- cgit From 3ba718749c258e48d83d7c2fe6b2dc08f165a29a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Dec 2019 10:35:08 -0500 Subject: don't keep a reference to uncached assets --- .../ContentManagers/BaseContentManager.cs | 12 ++++-- .../ContentManagers/GameContentManager.cs | 47 ++++++++++++---------- 2 files changed, 33 insertions(+), 26 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 93fd729b..4cfeeeba 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -258,20 +258,24 @@ namespace StardewModdingAPI.Framework.ContentManagers : base.ReadAsset(assetName, disposable => this.Disposables.Add(new WeakReference(disposable))); } - /// Inject an asset into the cache. + /// Add tracking data to an asset and add it to 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 language code for which to inject the asset. - protected virtual void Inject(string assetName, T value, LanguageCode language) + /// Whether to save the asset to the asset cache. + protected virtual void TrackAsset(string assetName, T value, LanguageCode language, bool useCache) { // track asset key if (value is Texture2D texture) texture.Name = assetName; // cache asset - assetName = this.AssertAndNormalizeAssetName(assetName); - this.Cache[assetName] = value; + if (useCache) + { + assetName = this.AssertAndNormalizeAssetName(assetName); + this.Cache[assetName] = value; + } } /// Parse a cache key into its component parts. diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 0b563555..04c4564f 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -83,8 +83,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) { T managedAsset = this.Coordinator.LoadManagedAsset(contentManagerID, relativePath); - if (useCache) - this.Inject(assetName, managedAsset, language); + this.TrackAsset(assetName, managedAsset, language, useCache); return managedAsset; } @@ -111,7 +110,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } // update cache & return data - this.Inject(assetName, data, language); + this.TrackAsset(assetName, data, language, useCache); return data; } @@ -169,18 +168,19 @@ namespace StardewModdingAPI.Framework.ContentManagers return false; } - /// Inject an asset into the cache. + /// Add tracking data to an asset and add it to 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 language code for which to inject the asset. - protected override void Inject(string assetName, T value, LanguageCode language) + /// Whether to save the asset to the asset cache. + protected override void TrackAsset(string assetName, T value, LanguageCode language, bool useCache) { // handle explicit language in asset name { if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) { - this.Inject(newAssetName, value, newLanguage); + this.TrackAsset(newAssetName, value, newLanguage, useCache); return; } } @@ -192,24 +192,27 @@ namespace StardewModdingAPI.Framework.ContentManagers // only caches by the most specific key). // 2. Because a mod asset loader/editor may have changed the asset in a way that // doesn't change the instance stored in the cache, e.g. using `asset.ReplaceWith`. - string keyWithLocale = $"{assetName}.{this.GetLocale(language)}"; - base.Inject(assetName, value, language); - if (this.Cache.ContainsKey(keyWithLocale)) - base.Inject(keyWithLocale, value, language); - - // track whether the injected asset is translatable for is-loaded lookups - if (this.Cache.ContainsKey(keyWithLocale)) - { - this.IsLocalizableLookup[assetName] = true; - this.IsLocalizableLookup[keyWithLocale] = true; - } - else if (this.Cache.ContainsKey(assetName)) + if (useCache) { - this.IsLocalizableLookup[assetName] = false; - this.IsLocalizableLookup[keyWithLocale] = false; + string keyWithLocale = $"{assetName}.{this.GetLocale(language)}"; + base.TrackAsset(assetName, value, language, useCache: true); + if (this.Cache.ContainsKey(keyWithLocale)) + base.TrackAsset(keyWithLocale, value, language, useCache: true); + + // track whether the injected asset is translatable for is-loaded lookups + if (this.Cache.ContainsKey(keyWithLocale)) + { + this.IsLocalizableLookup[assetName] = true; + this.IsLocalizableLookup[keyWithLocale] = true; + } + else if (this.Cache.ContainsKey(assetName)) + { + this.IsLocalizableLookup[assetName] = false; + this.IsLocalizableLookup[keyWithLocale] = false; + } + else + this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error); } - else - this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error); } /// Load an asset file directly from the underlying content manager. -- cgit From 6dc442803fe4fbe2a38b9fb287990cc8692c17eb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Dec 2019 10:38:17 -0500 Subject: fix private assets from content packs not having tracking info --- .../Framework/ContentManagers/ModContentManager.cs | 30 +++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 90b86179..fdf76b24 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -105,6 +105,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // get local asset SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}"); + T asset; try { // get file @@ -118,22 +119,22 @@ namespace StardewModdingAPI.Framework.ContentManagers // XNB file case ".xnb": { - T data = this.RawLoad(assetName, useCache: false); - if (data is Map map) + asset = this.RawLoad(assetName, useCache: false); + if (asset is Map map) { this.NormalizeTilesheetPaths(map); this.FixCustomTilesheetPaths(map, relativeMapPath: assetName); } - return data; } + break; // unpacked data case ".json": { - if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T data)) + if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out asset)) throw GetContentError("the JSON file is invalid."); // should never happen since we check for file existence above - return data; } + break; // unpacked image case ".png": @@ -143,13 +144,13 @@ namespace StardewModdingAPI.Framework.ContentManagers 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); - return (T)(object)texture; - } + using FileStream stream = File.OpenRead(file.FullName); + + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + asset = (T)(object)texture; } + break; // unpacked map case ".tbin": @@ -163,8 +164,9 @@ namespace StardewModdingAPI.Framework.ContentManagers Map map = formatManager.LoadMap(file.FullName); this.NormalizeTilesheetPaths(map); this.FixCustomTilesheetPaths(map, relativeMapPath: assetName); - return (T)(object)map; + asset = (T)(object)map; } + break; default: throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.json', '.png', '.tbin', or '.xnb'."); @@ -176,6 +178,10 @@ namespace StardewModdingAPI.Framework.ContentManagers throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); throw new SContentLoadException($"The content manager failed loading content asset '{assetName}' from {this.Name}.", ex); } + + // track & return asset + this.TrackAsset(assetName, asset, language, useCache); + return asset; } /// Create a new content manager for temporary use. -- cgit From 16f986c51b9c87c2253a39fd771dcc24f7c43db4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Dec 2019 21:31:34 -0500 Subject: refactor cache invalidation & propagation to allow for future optimizations --- .../Framework/ContentManagers/BaseContentManager.cs | 16 ++++++++-------- .../Framework/ContentManagers/GameContentManager.cs | 2 +- src/SMAPI/Framework/ContentManagers/IContentManager.cs | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 4cfeeeba..41ce7c37 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -184,25 +184,25 @@ namespace StardewModdingAPI.Framework.ContentManagers /// 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 the invalidated asset names and types. - public IEnumerable> InvalidateCache(Func predicate, bool dispose = false) + /// Returns the invalidated asset names and instances. + public IDictionary InvalidateCache(Func predicate, bool dispose = false) { - Dictionary removeAssetNames = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - this.Cache.Remove((key, type) => + IDictionary removeAssets = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, asset) => { this.ParseCacheKey(key, out string assetName, out _); - if (removeAssetNames.ContainsKey(assetName)) + if (removeAssets.ContainsKey(assetName)) return true; - if (predicate(assetName, type)) + if (predicate(assetName, asset.GetType())) { - removeAssetNames[assetName] = type; + removeAssets[assetName] = asset; return true; } return false; }, dispose); - return removeAssetNames.Select(p => Tuple.Create(p.Key, p.Value)); + return removeAssets; } /// Dispose held resources. diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 04c4564f..8930267d 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -130,7 +130,7 @@ namespace StardewModdingAPI.Framework.ContentManagers removeAssetNames.Contains(key) || (this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) && removeAssetNames.Contains(assetName)) ) - .Select(p => p.Item1) + .Select(p => p.Key) .OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase) .ToArray(); if (invalidated.Any()) diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 12c01352..8da9a777 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// 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 the invalidated asset names and types. - IEnumerable> InvalidateCache(Func predicate, bool dispose = false); + /// Returns the invalidated asset names and instances. + IDictionary InvalidateCache(Func predicate, bool dispose = false); } } -- cgit From dca60f42b2048d6b0b27517b9e7686665e61e9c2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 31 Dec 2019 16:18:11 -0500 Subject: fix XNA keeping loaded assets alive forever (#685) --- src/SMAPI/Framework/ContentManagers/BaseContentManager.cs | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src/SMAPI/Framework/ContentManagers') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 41ce7c37..36f2f650 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -41,6 +41,10 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A list of disposable assets. private readonly List> Disposables = new List>(); + /// The disposable assets tracked by the base content manager. + /// This should be kept empty to avoid keeping disposable assets referenced forever, which prevents garbage collection when they're unused. Disposable assets are tracked by instead, which avoids a hard reference. + private readonly List BaseDisposableReferences; + /********* ** Accessors @@ -84,6 +88,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // get asset data this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); + this.BaseDisposableReferences = reflection.GetField>(this, "disposableAssets").GetValue(); } /// Load an asset that has been processed by the content pipeline. @@ -276,6 +281,9 @@ namespace StardewModdingAPI.Framework.ContentManagers assetName = this.AssertAndNormalizeAssetName(assetName); this.Cache[assetName] = value; } + + // avoid hard disposable references; see remarks on the field + this.BaseDisposableReferences.Clear(); } /// Parse a cache key into its component parts. -- cgit