From 0bcb99f2c46cf08e0c05e1cd27a0662a4a1e375b Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sat, 30 Sep 2023 01:16:56 +0300
Subject: [PATCH 1/3] Crop oversized gameplay textures instead of downscaling
 them

---
 osu.Game/Skinning/LegacySkinExtensions.cs | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs
index dde6c1fa29..868f36fb34 100644
--- a/osu.Game/Skinning/LegacySkinExtensions.cs
+++ b/osu.Game/Skinning/LegacySkinExtensions.cs
@@ -9,6 +9,7 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Primitives;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.Textures;
 using osuTK;
@@ -112,9 +113,11 @@ namespace osu.Game.Skinning
             if (texture.DisplayWidth <= maxSize.X && texture.DisplayHeight <= maxSize.Y)
                 return texture;
 
-            // use scale adjust property for downscaling the texture in order to meet the specified maximum dimensions.
-            texture.ScaleAdjust *= Math.Max(texture.DisplayWidth / maxSize.X, texture.DisplayHeight / maxSize.Y);
-            return texture;
+            maxSize *= texture.ScaleAdjust;
+
+            var croppedTexture = texture.Crop(new RectangleF(texture.Width / 2f - maxSize.X / 2f, texture.Height / 2f - maxSize.Y / 2f, maxSize.X, maxSize.Y));
+            croppedTexture.ScaleAdjust = texture.ScaleAdjust;
+            return croppedTexture;
         }
 
         public static bool HasFont(this ISkin source, LegacyFont font)

From 314ecec65b34c73a293e7f8fbf14183c5dcfa6b4 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sat, 30 Sep 2023 01:17:59 +0300
Subject: [PATCH 2/3] Refactor player max dimensions test scene to actually
 upscale textures

---
 .../Gameplay/TestScenePlayerMaxDimensions.cs  | 77 ++++++++++++++++---
 osu.Game/Skinning/Skin.cs                     |  5 +-
 2 files changed, 71 insertions(+), 11 deletions(-)

diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs
index a8ed44c7f8..68443b234b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs
@@ -3,14 +3,20 @@
 
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics.Textures;
+using osu.Framework.IO.Stores;
 using osu.Framework.Testing;
 using osu.Game.IO;
 using osu.Game.Rulesets;
 using osu.Game.Screens.Play;
 using osu.Game.Skinning;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
 
 namespace osu.Game.Tests.Visual.Gameplay
 {
@@ -23,6 +29,9 @@ namespace osu.Game.Tests.Visual.Gameplay
     /// </remarks>
     public partial class TestScenePlayerMaxDimensions : TestSceneAllRulesetPlayers
     {
+        // scale textures to 4 times their size.
+        private const int scale_factor = 4;
+
         protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
         {
             var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@@ -63,18 +72,66 @@ namespace osu.Game.Tests.Visual.Gameplay
                 remove { }
             }
 
-            public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
-            {
-                var texture = base.GetTexture(componentName, wrapModeS, wrapModeT);
-
-                if (texture != null)
-                    texture.ScaleAdjust /= 8f;
-
-                return texture;
-            }
-
             public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => this;
             public IEnumerable<ISkin> AllSources => new[] { this };
+
+            protected override IResourceStore<TextureUpload> CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore<byte[]> storage)
+                => new UpscaledTextureLoaderStore(base.CreateTextureLoaderStore(resources, storage));
+
+            private class UpscaledTextureLoaderStore : IResourceStore<TextureUpload>
+            {
+                private readonly IResourceStore<TextureUpload>? textureStore;
+
+                public UpscaledTextureLoaderStore(IResourceStore<TextureUpload>? textureStore)
+                {
+                    this.textureStore = textureStore;
+                }
+
+                public void Dispose()
+                {
+                    textureStore?.Dispose();
+                }
+
+                public TextureUpload Get(string name)
+                {
+                    var textureUpload = textureStore?.Get(name);
+
+                    // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
+                    if (textureUpload == null)
+                        return null!;
+
+                    return upscale(textureUpload);
+                }
+
+                public async Task<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken())
+                {
+                    // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
+                    if (textureStore == null)
+                        return null!;
+
+                    var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false);
+
+                    if (textureUpload == null)
+                        return null!;
+
+                    return await Task.Run(() => upscale(textureUpload), cancellationToken).ConfigureAwait(false);
+                }
+
+                private TextureUpload upscale(TextureUpload textureUpload)
+                {
+                    var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
+
+                    // The original texture upload will no longer be returned or used.
+                    textureUpload.Dispose();
+
+                    image.Mutate(i => i.Resize(new Size(textureUpload.Width, textureUpload.Height) * scale_factor));
+                    return new TextureUpload(image);
+                }
+
+                public Stream? GetStream(string name) => textureStore?.GetStream(name);
+
+                public IEnumerable<string> GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty<string>();
+            }
         }
     }
 }
diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs
index ccf49d722f..1e312142d7 100644
--- a/osu.Game/Skinning/Skin.cs
+++ b/osu.Game/Skinning/Skin.cs
@@ -88,7 +88,7 @@ namespace osu.Game.Skinning
                 }
 
                 Samples = samples;
-                Textures = new TextureStore(resources.Renderer, new MaxDimensionLimitedTextureLoaderStore(resources.CreateTextureLoaderStore(storage)));
+                Textures = new TextureStore(resources.Renderer, CreateTextureLoaderStore(resources, storage));
             }
             else
             {
@@ -171,6 +171,9 @@ namespace osu.Game.Skinning
             }
         }
 
+        protected virtual IResourceStore<TextureUpload> CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore<byte[]> storage)
+            => new MaxDimensionLimitedTextureLoaderStore(resources.CreateTextureLoaderStore(storage));
+
         protected virtual void ParseConfigurationStream(Stream stream)
         {
             using (LineBufferedReader reader = new LineBufferedReader(stream, true))

From 5a0faaa0b118e4935c05a78e00ec4214c69a6b5f Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 5 Oct 2023 14:30:04 +0900
Subject: [PATCH 3/3] Fix `TestReplayExport` intermittent failure

The previous fix was not working as it was checking the path for the
prefix `_`, not the filename.

See https://github.com/ppy/osu/runs/17415814653#r0s2 which clearly shows
this.
---
 .../Visual/Gameplay/TestScenePlayerLocalScoreImport.cs         | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
index feda251744..6a7fab86d6 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.IO;
 using System.Linq;
 using NUnit.Framework;
 using osu.Framework.Allocation;
@@ -173,7 +174,7 @@ namespace osu.Game.Tests.Visual.Gameplay
             string? filePath = null;
 
             // Files starting with _ are temporary, created by CreateFileSafely call.
-            AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !f.StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null);
+            AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null);
             AddAssert("filesize is non-zero", () =>
             {
                 using (var stream = LocalStorage.GetStream(filePath))