From a546fd113f431bd8888da50aad087213193c937e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 25 May 2022 18:02:48 -0400 Subject: [PATCH] add experimental image load rewrite --- docs/release-notes.md | 4 ++ src/SMAPI/Framework/ContentCoordinator.cs | 10 ++++- .../ContentManagers/ModContentManager.cs | 41 ++++++++++++++++--- src/SMAPI/Framework/Models/SConfig.cs | 8 +++- src/SMAPI/Framework/SCore.cs | 3 +- src/SMAPI/SMAPI.config.json | 5 +++ src/SMAPI/SMAPI.csproj | 1 + 7 files changed, 63 insertions(+), 9 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index e1aa47ab..a1b5222e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ # Release notes ## Upcoming release +* For players: + * Added experimental image load rewrite (disabled by default). + _If you have many content mods installed, enabling `UseExperimentalImageLoading` in `smapi-internal/config.json` may reduce load times or stutters when they load many image files at once._ + * For mod authors: * Fixed map edits which change warps sometimes rebuilding the NPC pathfinding cache unnecessarily, which could cause a noticeable delay for players. * In `smapi-internal/config.json`, you can now enable verbose logging for specific mods (instead of all or nothing). diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 69a39ac7..3ad112cd 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -32,6 +32,9 @@ namespace StardewModdingAPI.Framework /// An asset key prefix for assets from SMAPI mod folders. private readonly string ManagedPrefix = "SMAPI"; + /// Whether to use a newer approach when loading image files from mod folder which may be faster. + private readonly bool UseExperimentalImageLoading; + /// Get a file lookup for the given directory. private readonly Func GetFileLookup; @@ -130,7 +133,8 @@ namespace StardewModdingAPI.Framework /// Get a file lookup for the given directory. /// A callback to invoke when any asset names have been invalidated from the cache. /// Get the load/edit operations to apply to an asset by querying registered event handlers. - public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, Func getFileLookup, Action> onAssetsInvalidated, Func requestAssetOperations) + /// Whether to use a newer approach when loading image files from mod folder which may be faster. + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, Func getFileLookup, Action> onAssetsInvalidated, Func requestAssetOperations, bool useExperimentalImageLoading) { this.GetFileLookup = getFileLookup; this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); @@ -141,6 +145,7 @@ namespace StardewModdingAPI.Framework this.OnAssetsInvalidated = onAssetsInvalidated; this.RequestAssetOperations = requestAssetOperations; this.FullRootDirectory = Path.Combine(Constants.GamePath, rootDirectory); + this.UseExperimentalImageLoading = useExperimentalImageLoading; this.ContentManagers.Add( this.MainContentManager = new GameContentManager( name: "Game1.content", @@ -219,7 +224,8 @@ namespace StardewModdingAPI.Framework reflection: this.Reflection, jsonHelper: this.JsonHelper, onDisposing: this.OnDisposing, - fileLookup: this.GetFileLookup(rootDirectory) + fileLookup: this.GetFileLookup(rootDirectory), + useExperimentalImageLoading: this.UseExperimentalImageLoading ); this.ContentManagers.Add(manager); return manager; diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 1b94b8c6..055dcc5f 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -7,6 +7,7 @@ using BmFont; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using SkiaSharp; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Serialization; @@ -25,6 +26,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /********* ** Fields *********/ + /// Whether to use a newer approach when loading image files from mod folder which may be faster. + private readonly bool UseExperimentalImageLoading; + /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; @@ -57,13 +61,15 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Encapsulates SMAPI's JSON file parsing. /// A callback to invoke when the content manager is being disposed. /// A lookup for files within the . - public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing, IFileLookup fileLookup) + /// Whether to use a newer approach when loading image files from mod folder which may be faster. + public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing, IFileLookup fileLookup, bool useExperimentalImageLoading) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) { this.GameContentManager = gameContentManager; this.FileLookup = fileLookup; this.JsonHelper = jsonHelper; this.ModName = modName; + this.UseExperimentalImageLoading = useExperimentalImageLoading; this.TryLocalizeKeys = false; } @@ -187,10 +193,35 @@ namespace StardewModdingAPI.Framework.ContentManagers throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); // load - using FileStream stream = File.OpenRead(file.FullName); - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); - return (T)(object)texture; + if (this.UseExperimentalImageLoading) + { + // load raw data + using FileStream stream = File.OpenRead(file.FullName); + using SKBitmap bitmap = SKBitmap.Decode(stream); + SKPMColor[] rawPixels = SKPMColor.PreMultiply(bitmap.Pixels); + + // convert to XNA pixel format + Color[] pixels = new Color[rawPixels.Length]; + for (int i = pixels.Length - 1; i >= 0; i--) + { + SKPMColor pixel = rawPixels[i]; + pixels[i] = pixel.Alpha == 0 + ? Color.Transparent + : new Color(r: pixel.Red, g: pixel.Green, b: pixel.Blue, alpha: pixel.Alpha); + } + + // create texture + Texture2D texture = new(Game1.graphics.GraphicsDevice, bitmap.Width, bitmap.Height); + texture.SetData(pixels); + return (T)(object)texture; + } + else + { + using FileStream stream = File.OpenRead(file.FullName); + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + return (T)(object)texture; + } } /// Load an unpacked image file (.tbin or .tmx). diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 240af002..f12da0a7 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -23,6 +23,7 @@ namespace StardewModdingAPI.Framework.Models [nameof(LogNetworkTraffic)] = false, [nameof(RewriteMods)] = true, [nameof(UsePintail)] = true, + [nameof(UseExperimentalImageLoading)] = false, [nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux }; @@ -66,6 +67,9 @@ namespace StardewModdingAPI.Framework.Models /// Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself. public bool UsePintail { get; } + /// Whether to use a newer approach when loading image files from mod folder which may be faster. + public bool UseExperimentalImageLoading { get; } + /// Whether to make SMAPI file APIs case-insensitive, even on Linux. public bool UseCaseInsensitivePaths { get; } @@ -92,11 +96,12 @@ namespace StardewModdingAPI.Framework.Models /// The log contexts for which to enable verbose logging, which may show a lot more information to simplify troubleshooting. /// Whether SMAPI should rewrite mods for compatibility. /// Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself. + /// Whether to use a newer approach when loading image files from mod folder which may be faster. /// >Whether to make SMAPI file APIs case-insensitive, even on Linux. /// Whether SMAPI should log network traffic. /// The colors to use for text written to the SMAPI console. /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. - public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? usePintail, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks) + public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? usePintail, bool? useExperimentalImageLoading, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)]; @@ -107,6 +112,7 @@ namespace StardewModdingAPI.Framework.Models this.VerboseLogging = new HashSet(verboseLogging ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(this.RewriteMods)]; this.UsePintail = usePintail ?? (bool)SConfig.DefaultValues[nameof(this.UsePintail)]; + this.UseExperimentalImageLoading = useExperimentalImageLoading ?? (bool)SConfig.DefaultValues[nameof(this.UseExperimentalImageLoading)]; this.UseCaseInsensitivePaths = useCaseInsensitivePaths ?? (bool)SConfig.DefaultValues[nameof(this.UseCaseInsensitivePaths)]; this.LogNetworkTraffic = logNetworkTraffic ?? (bool)SConfig.DefaultValues[nameof(this.LogNetworkTraffic)]; this.ConsoleColors = consoleColors; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 4f4212dc..242776b3 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1301,7 +1301,8 @@ namespace StardewModdingAPI.Framework onAssetLoaded: this.OnAssetLoaded, onAssetsInvalidated: this.OnAssetsInvalidated, getFileLookup: this.GetFileLookup, - requestAssetOperations: this.RequestAssetOperations + requestAssetOperations: this.RequestAssetOperations, + useExperimentalImageLoading: this.Settings.UseExperimentalImageLoading ); if (this.ContentCore.Language != this.Translator.LocaleEnum) this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language); diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index a6ec42d3..8e710435 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -60,6 +60,11 @@ copy all the settings, or you may cause bugs due to overridden changes in future */ "UsePintail": true, + /** + * Whether to use a newer approach when loading image files from mod folder which may be faster. + */ + "UseExperimentalImageLoading": false, + /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index a0ca54cc..91e4c668 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -41,6 +41,7 @@ +