From 423f2352af9d0e9815daf4ba3ba33134f587ce47 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 31 Jan 2021 22:08:03 -0500 Subject: rework aggressive memory optimization to minimize mod impact (#757) --- .../GameContentManagerForAssetPropagation.cs | 47 ++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs (limited to 'src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs') diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs new file mode 100644 index 00000000..cbbebf02 --- /dev/null +++ b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs @@ -0,0 +1,47 @@ +using System; +using System.Globalization; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// An extension of specifically optimized for asset propagation. + /// This avoids sharing an asset cache with or mods, so that assets can be safely disposed when the vanilla game no longer references them. + internal class GameContentManagerForAssetPropagation : GameContentManager + { + /********* + ** Fields + *********/ + /// A unique value used in to identify assets loaded through this instance. + private readonly string Tag = $"Pathoschild.SMAPI/LoadedBy:{nameof(GameContentManagerForAssetPropagation)}"; + + + /********* + ** Public methods + *********/ + /// + public GameContentManagerForAssetPropagation(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, Action onLoadingFirstAsset, bool aggressiveMemoryOptimizations) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, onLoadingFirstAsset, aggressiveMemoryOptimizations) { } + + /// + public override T Load(string assetName, LanguageCode language, bool useCache) + { + T data = base.Load(assetName, language, useCache); + + if (data is Texture2D texture) + texture.Tag = this.Tag; + + return data; + } + + /// Get whether a texture was loaded by this content manager. + /// The texture to check. + public bool IsReponsibleFor(Texture2D texture) + { + return + texture?.Tag is string tag + && tag.Contains(this.Tag); + } + } +} -- cgit From ad80f8b078176519d49732b28a03465e6d8a6501 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 21 Feb 2021 13:50:08 -0500 Subject: use inheritdoc, minor cleanup --- .../ContentManagers/BaseContentManager.cs | 56 +++++++--------------- .../ContentManagers/GameContentManager.cs | 27 ++++------- .../GameContentManagerForAssetPropagation.cs | 2 +- .../Framework/ContentManagers/ModContentManager.cs | 21 ++------ src/SMAPI/Metadata/CoreAssetPropagator.cs | 2 +- 5 files changed, 32 insertions(+), 76 deletions(-) (limited to 'src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index b6b3a7a0..1a64dab8 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -5,7 +5,6 @@ using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; -using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Exceptions; @@ -53,16 +52,16 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Accessors *********/ - /// A name for the mod manager. Not guaranteed to be unique. + /// public string Name { get; } - /// The current language as a constant. + /// public LanguageCode Language => this.GetCurrentLanguage(); - /// The absolute path to the . + /// public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); - /// Whether this content manager can be targeted by managed asset keys (e.g. to load assets from a mod folder). + /// public bool IsNamespaced { get; } @@ -97,56 +96,42 @@ namespace StardewModdingAPI.Framework.ContentManagers this.BaseDisposableReferences = reflection.GetField>(this, "disposableAssets").GetValue(); } - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. + /// public override T Load(string assetName) { return this.Load(assetName, this.Language, useCache: true); } - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The language code for which to load content. + /// public override T Load(string assetName, LanguageCode language) { return this.Load(assetName, language, useCache: true); } - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The language code for which to load content. - /// Whether to read/write the loaded asset to the asset cache. + /// public abstract T Load(string assetName, LocalizedContentManager.LanguageCode language, bool useCache); - /// Load the base asset without localization. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. + /// [Obsolete("This method is implemented for the base game and should not be used directly. To load an asset from the underlying content manager directly, use " + nameof(BaseContentManager.RawLoad) + " instead.")] public override T LoadBase(string assetName) { return this.Load(assetName, LanguageCode.en, useCache: true); } - /// Perform any cleanup needed when the locale changes. + /// public virtual void OnLocaleChanged() { } /// public virtual void OnReturningToTitleScreen() { } - /// Normalize path separators in a file path. For asset keys, see instead. - /// The file path to normalize. + /// [Pure] public string NormalizePathSeparators(string path) { return this.Cache.NormalizePathSeparators(path); } - /// Assert that the given key has a valid format and return a normalized form consistent with the underlying cache. - /// 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 string AssertAndNormalizeAssetName(string assetName) { @@ -163,29 +148,26 @@ namespace StardewModdingAPI.Framework.ContentManagers /**** ** Content loading ****/ - /// Get the current content locale. + /// public string GetLocale() { return this.GetLocale(this.GetCurrentLanguage()); } - /// The locale for a language. - /// The language. + /// public string GetLocale(LanguageCode language) { return this.LanguageCodeString(language); } - /// 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. - /// The language. + /// public bool IsLoaded(string assetName, LanguageCode language) { assetName = this.Cache.NormalizeKey(assetName); return this.IsNormalizedKeyLoaded(assetName, language); } - /// Get the cached asset keys. + /// public IEnumerable GetAssetKeys() { return this.Cache.Keys @@ -196,10 +178,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /**** ** Cache invalidation ****/ - /// 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 instances. + /// public IDictionary InvalidateCache(Func predicate, bool dispose = false) { IDictionary removeAssets = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -228,8 +207,7 @@ namespace StardewModdingAPI.Framework.ContentManagers return removeAssets; } - /// Dispose held resources. - /// Whether the content manager is being disposed (rather than finalized). + /// protected override void Dispose(bool isDisposing) { // ignore if disposed diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index e3d1e569..8e78faba 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -59,11 +59,7 @@ namespace StardewModdingAPI.Framework.ContentManagers this.OnLoadingFirstAsset = onLoadingFirstAsset; } - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The language code for which to load content. - /// Whether to read/write the loaded asset to the asset cache. + /// public override T Load(string assetName, LocalizedContentManager.LanguageCode language, bool useCache) { // raise first-load callback @@ -95,7 +91,7 @@ namespace StardewModdingAPI.Framework.ContentManagers 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); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}"); data = this.RawLoad(assetName, language, useCache); } else @@ -117,7 +113,7 @@ namespace StardewModdingAPI.Framework.ContentManagers return data; } - /// Perform any cleanup needed when the locale changes. + /// public override void OnLocaleChanged() { base.OnLocaleChanged(); @@ -137,7 +133,7 @@ namespace StardewModdingAPI.Framework.ContentManagers .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) .ToArray(); if (invalidated.Any()) - this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change.", LogLevel.Trace); + this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change."); } /// @@ -165,7 +161,7 @@ namespace StardewModdingAPI.Framework.ContentManagers this.InvalidateCache((_, _) => true); } - /// Create a new content manager for temporary use. + /// public override LocalizedContentManager CreateTemporary() { return this.Coordinator.CreateGameContentManager("(temporary)"); @@ -175,9 +171,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Private methods *********/ - /// Get whether an asset has already been loaded. - /// The normalized asset name. - /// The language to check. + /// protected override bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language) { string cachedKey = null; @@ -191,12 +185,7 @@ namespace StardewModdingAPI.Framework.ContentManagers : this.Cache.ContainsKey(normalizedAssetName); } - /// 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. - /// 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 @@ -384,7 +373,7 @@ namespace StardewModdingAPI.Framework.ContentManagers try { editor.Edit(asset); - this.Monitor.Log($"{mod.DisplayName} edited {info.AssetName}.", LogLevel.Trace); + this.Monitor.Log($"{mod.DisplayName} edited {info.AssetName}."); } catch (Exception ex) { diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs index cbbebf02..61683ce6 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Get whether a texture was loaded by this content manager. /// The texture to check. - public bool IsReponsibleFor(Texture2D texture) + public bool IsResponsibleFor(Texture2D texture) { return texture?.Tag is string tag diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 284c1f37..9af14cb5 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -59,28 +59,19 @@ namespace StardewModdingAPI.Framework.ContentManagers this.ModName = modName; } - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. + /// public override T Load(string assetName) { return this.Load(assetName, this.DefaultLanguage, useCache: false); } - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The language code for which to load content. + /// public override T Load(string assetName, LanguageCode language) { return this.Load(assetName, language, useCache: false); } - /// Load an asset that has been processed by the content pipeline. - /// The type of asset to load. - /// The asset path relative to the loader root directory, not including the .xnb extension. - /// The language code for which to load content. - /// Whether to read/write the loaded asset to the asset cache. + /// public override T Load(string assetName, LanguageCode language, bool useCache) { assetName = this.AssertAndNormalizeAssetName(assetName); @@ -190,7 +181,7 @@ namespace StardewModdingAPI.Framework.ContentManagers return asset; } - /// Create a new content manager for temporary use. + /// public override LocalizedContentManager CreateTemporary() { throw new NotSupportedException("Can't create a temporary mod content manager."); @@ -210,9 +201,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Private methods *********/ - /// Get whether an asset has already been loaded. - /// The normalized asset name. - /// The language to check. + /// protected override bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language) { return this.Cache.ContainsKey(normalizedAssetName); diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 5fb1b10d..8b591bc1 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -1201,7 +1201,7 @@ namespace StardewModdingAPI.Metadata GameContentManagerForAssetPropagation content = this.DisposableContentManager; Texture2D newTexture = content.Load(key); - if (oldTexture?.IsDisposed == false && !object.ReferenceEquals(oldTexture, newTexture) && content.IsReponsibleFor(oldTexture)) + if (oldTexture?.IsDisposed == false && !object.ReferenceEquals(oldTexture, newTexture) && content.IsResponsibleFor(oldTexture)) oldTexture.Dispose(); return newTexture; -- cgit