A game framework written with osu! in mind.

Merge branch 'master' into tabcontrol-initial-autosort

Changed files
+310 -82
osu.Framework
osu.Framework.Android
osu.Framework.Tests
osu.Framework.iOS
+12 -9
README.md
··· 11 11 12 12 A game framework written with [osu!](https://github.com/ppy/osu) in mind. 13 13 14 - ## Requirements 14 + ## Developing a game using osu!framework 15 + 16 + If you are interested in **creating a project** using the framework, please start from the [getting started](https://github.com/ppy/osu-framework/wiki/Setting-up-your-first-project) wiki resources (or jump straight over to the [project templates](https://github.com/ppy/osu-framework/tree/master/osu.Framework.Templates). You can either start off from an empty project, or take a peek at a working sample game. Either way, full project structure, cross-platform support, and a testing setup are included! 15 17 16 - - A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) or higher installed. 17 - - When running on linux, please have a system-wide ffmpeg installation available to support video decoding. 18 - - When running on Windows 7 or 8.1, *[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net50&pivots=os-windows#dependencies)** may be required to correctly run .NET 5 applications if your operating system is not up-to-date with the latest service packs. 19 - - When working with the codebase, we recommend using an IDE with intellisense and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [Jetbrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). 18 + The rest of the information on this page is related to working *on* the framework, not *using* it! 20 19 21 20 ## Objectives 22 21 ··· 26 25 - Common elements used by games (texture caching, font loading) will be automatically initialised at runtime. 27 26 - Allow for isolated development of components via a solid testing environment (`VisualTests` and `TestCases`). Check the [wiki](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) for more information on how these can be used to streamline development. 28 27 28 + ## Requirements 29 + 30 + - A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) or higher installed. 31 + - When running on linux, please have a system-wide ffmpeg installation available to support video decoding. 32 + - When running on Windows 7 or 8.1, *[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net50&pivots=os-windows#dependencies)** may be required to correctly run .NET 5 applications if your operating system is not up-to-date with the latest service packs. 33 + - When working with the codebase, we recommend using an IDE with intellisense and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [Jetbrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). 34 + 29 35 ### Building 30 36 31 37 Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this provided [below](#contributing). ··· 54 60 55 61 The BASS audio library (a dependency of this framework) is a commercial product. While it is free for non-commercial use, please ensure to [obtain a valid licence](http://www.un4seen.com/bass.html#license) if you plan on distributing any application using it commercially. 56 62 57 - ## Developing a game using osu!framework 58 - 59 - If you want to get started making your own game project using osu!framework, check out our [project templates](https://github.com/ppy/osu-framework/tree/master/osu.Framework.Templates). You can either start off from an empty project, or take a peek at a working sample game. Either way, full project structure, cross-platform support, and testing setup are included! 60 - 61 63 ## Projects that use osu!framework 62 64 63 65 [osu!](https://github.com/ppy/osu) – rhythm is just a *click* away! ··· 75 77 - Must be a GitHub link (i.e. your project is open source) 76 78 - Must be actively developed (and have executable releases) 77 79 --> 80 +
+4 -1
osu.Framework.Android/AndroidGameHost.cs
··· 59 59 60 60 public override Storage GetStorage(string path) => new AndroidStorage(path, this); 61 61 62 - public override string UserStoragePath => Application.Context.GetExternalFilesDir(string.Empty).ToString(); 62 + public override IEnumerable<string> UserStoragePaths => new[] 63 + { 64 + Application.Context.GetExternalFilesDir(string.Empty).ToString() 65 + }; 63 66 64 67 public override void OpenFileExternally(string filename) 65 68 => throw new NotImplementedException();
+150
osu.Framework.Tests/Platform/UserStorageLookupTest.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System; 5 + using System.Collections.Generic; 6 + using System.IO; 7 + using NUnit.Framework; 8 + using osu.Framework.Testing; 9 + 10 + namespace osu.Framework.Tests.Platform 11 + { 12 + [TestFixture] 13 + public class UserStorageLookupTest 14 + { 15 + private static readonly string path1 = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, "path1"); 16 + private static readonly string path2 = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, "path2"); 17 + 18 + private const string game_name = "test_game"; 19 + 20 + [TearDown] 21 + public void TearDown() 22 + { 23 + try 24 + { 25 + File.Delete(path1); 26 + File.Delete(path2); 27 + } 28 + catch 29 + { 30 + } 31 + 32 + try 33 + { 34 + Directory.Delete(path1, true); 35 + Directory.Delete(path2, true); 36 + } 37 + catch 38 + { 39 + } 40 + } 41 + 42 + [Test] 43 + public void TestFirstBaseExisting() 44 + { 45 + Directory.CreateDirectory(path1); 46 + 47 + using (var host = new StorageLookupHeadlessGameHost()) 48 + { 49 + runHost(host); 50 + Assert.IsTrue(host.Storage.GetFullPath(string.Empty).StartsWith(path1, StringComparison.Ordinal)); 51 + } 52 + } 53 + 54 + [Test] 55 + public void TestSecondBaseExistingStillPrefersFirst() 56 + { 57 + Directory.CreateDirectory(path2); 58 + 59 + using (var host = new StorageLookupHeadlessGameHost()) 60 + { 61 + runHost(host); 62 + Assert.IsTrue(host.Storage.GetFullPath(string.Empty).StartsWith(path1, StringComparison.Ordinal)); 63 + } 64 + } 65 + 66 + [Test] 67 + public void TestSecondBaseUsedIfFirstFails() 68 + { 69 + // write a file so directory creation fails. 70 + File.WriteAllText(path1, ""); 71 + 72 + using (var host = new StorageLookupHeadlessGameHost()) 73 + { 74 + runHost(host); 75 + Assert.IsTrue(host.Storage.GetFullPath(string.Empty).StartsWith(path2, StringComparison.Ordinal)); 76 + } 77 + } 78 + 79 + [Test] 80 + public void TestFirstDataExisting() 81 + { 82 + Directory.CreateDirectory(Path.Combine(path1, game_name)); 83 + 84 + using (var host = new StorageLookupHeadlessGameHost()) 85 + { 86 + runHost(host); 87 + Assert.IsTrue(host.Storage.GetFullPath(string.Empty).StartsWith(path1, StringComparison.Ordinal)); 88 + } 89 + } 90 + 91 + [Test] 92 + public void TestSecondDataExisting() 93 + { 94 + Directory.CreateDirectory(Path.Combine(path2, game_name)); 95 + 96 + using (var host = new StorageLookupHeadlessGameHost()) 97 + { 98 + runHost(host); 99 + Assert.IsTrue(host.Storage.GetFullPath(string.Empty).StartsWith(path2, StringComparison.Ordinal)); 100 + } 101 + } 102 + 103 + [Test] 104 + public void TestPrefersFirstData() 105 + { 106 + Directory.CreateDirectory(Path.Combine(path1, game_name)); 107 + Directory.CreateDirectory(Path.Combine(path2, game_name)); 108 + 109 + using (var host = new StorageLookupHeadlessGameHost()) 110 + { 111 + runHost(host); 112 + Assert.IsTrue(host.Storage.GetFullPath(string.Empty).StartsWith(path1, StringComparison.Ordinal)); 113 + } 114 + } 115 + 116 + [Test] 117 + public void TestPrefersSecondDataOverFirstBase() 118 + { 119 + Directory.CreateDirectory(path1); 120 + Directory.CreateDirectory(Path.Combine(path2, game_name)); 121 + 122 + using (var host = new StorageLookupHeadlessGameHost()) 123 + { 124 + runHost(host); 125 + Assert.IsTrue(host.Storage.GetFullPath(string.Empty).StartsWith(path2, StringComparison.Ordinal)); 126 + } 127 + } 128 + 129 + private static void runHost(StorageLookupHeadlessGameHost host) 130 + { 131 + TestGame game = new TestGame(); 132 + game.Schedule(() => game.Exit()); 133 + host.Run(game); 134 + } 135 + 136 + private class StorageLookupHeadlessGameHost : TestRunHeadlessGameHost 137 + { 138 + public StorageLookupHeadlessGameHost() 139 + : base(game_name) 140 + { 141 + } 142 + 143 + public override IEnumerable<string> UserStoragePaths => new[] 144 + { 145 + path1, 146 + path2, 147 + }; 148 + } 149 + } 150 + }
+19
osu.Framework.Tests/Visual/Sprites/TestSceneVideo.cs
··· 95 95 AddUntilStep("decoding ran", () => didDecode); 96 96 } 97 97 98 + [TestCase(false)] 99 + [TestCase(true)] 100 + public void TestDecodingStopsBeforeStartTime(bool looping) 101 + { 102 + AddStep("Set looping", () => video.Loop = looping); 103 + 104 + AddStep("Jump back to before start time", () => clock.CurrentTime = -30000); 105 + 106 + AddUntilStep("decoding stopped", () => video.State == VideoDecoder.DecoderState.Ready); 107 + 108 + AddStep("reset decode state", () => didDecode = false); 109 + 110 + AddWaitStep("wait a bit", 10); 111 + AddAssert("decoding didn't run", () => !didDecode); 112 + 113 + AddStep("seek close to start", () => clock.CurrentTime = -500); 114 + AddUntilStep("decoding ran", () => didDecode); 115 + } 116 + 98 117 [Test] 99 118 public void TestJumpForward() 100 119 {
-2
osu.Framework.iOS/IOSGameHost.cs
··· 107 107 108 108 public override Storage GetStorage(string path) => new IOSStorage(path, this); 109 109 110 - public override string UserStoragePath => Environment.GetFolderPath(Environment.SpecialFolder.Personal); 111 - 112 110 public override void OpenFileExternally(string filename) => throw new NotImplementedException(); 113 111 114 112 public override void OpenUrlExternally(string url)
+4 -3
osu.Framework/Graphics/Animations/AnimationClockComposite.cs
··· 83 83 { 84 84 get 85 85 { 86 - if (Loop) 87 - return manualClock.CurrentTime % Duration; 86 + double current = manualClock.CurrentTime; 87 + 88 + if (Loop) current %= Duration; 88 89 89 - return Math.Min(manualClock.CurrentTime, Duration); 90 + return Math.Clamp(current, 0, Duration); 90 91 } 91 92 set 92 93 {
+1 -1
osu.Framework/Graphics/Performance/FrameTimeDisplay.cs
··· 99 99 elapsedSinceLastUpdate = 0; 100 100 101 101 counter.Text = $"{displayFps:0}fps ({rollingElapsed:0.00}ms)" 102 - + (clock.Throttling ? $"{(clock.MaximumUpdateHz < 10000 ? clock.MaximumUpdateHz.ToString("0") : "∞").PadLeft(4)}hz" : string.Empty); 102 + + (clock.Throttling ? $"{(clock.MaximumUpdateHz > 0 && clock.MaximumUpdateHz < 10000 ? clock.MaximumUpdateHz.ToString("0") : "∞").PadLeft(4)}hz" : string.Empty); 103 103 } 104 104 105 105 private class CounterText : SpriteText
+15 -11
osu.Framework/Graphics/Video/Video.cs
··· 124 124 seekIntoSync(); 125 125 } 126 126 127 - var nextFrame = availableFrames.Count > 0 ? availableFrames.Peek() : null; 127 + var peekFrame = availableFrames.Count > 0 ? availableFrames.Peek() : null; 128 + bool outOfSync = false; 128 129 129 - if (nextFrame != null) 130 + if (peekFrame != null) 130 131 { 131 - bool tooFarBehind = Math.Abs(PlaybackPosition - nextFrame.Time) > lenience_before_seek && 132 - (!Loop || 133 - (Math.Abs(PlaybackPosition - decoder.Duration - nextFrame.Time) > lenience_before_seek && 134 - Math.Abs(PlaybackPosition + decoder.Duration - nextFrame.Time) > lenience_before_seek) 135 - ); 132 + outOfSync = Math.Abs(PlaybackPosition - peekFrame.Time) > lenience_before_seek; 136 133 137 - // we are too far ahead or too far behind 138 - if (tooFarBehind && decoder.CanSeek) 134 + if (Loop) 139 135 { 140 - Logger.Log($"Video too far out of sync ({nextFrame.Time}), seeking to {PlaybackPosition}"); 141 - seekIntoSync(); 136 + // handle looping bounds (as we could be in the roll-over process between loops). 137 + outOfSync &= Math.Abs(PlaybackPosition - decoder.Duration - peekFrame.Time) > lenience_before_seek && 138 + Math.Abs(PlaybackPosition + decoder.Duration - peekFrame.Time) > lenience_before_seek; 142 139 } 140 + } 141 + 142 + // we are too far ahead or too far behind 143 + if (outOfSync && decoder.CanSeek) 144 + { 145 + Logger.Log($"Video too far out of sync ({peekFrame.Time}), seeking to {PlaybackPosition}"); 146 + seekIntoSync(); 143 147 } 144 148 145 149 var frameTime = CurrentFrameTime;
+50 -2
osu.Framework/Platform/GameHost.cs
··· 136 136 /// <param name="path">The absolute path to be used as a root for the storage.</param> 137 137 public abstract Storage GetStorage(string path); 138 138 139 - public abstract string UserStoragePath { get; } 139 + /// <summary> 140 + /// All valid user storage paths in order of usage priority. 141 + /// </summary> 142 + public virtual IEnumerable<string> UserStoragePaths => Environment.GetFolderPath(Environment.SpecialFolder.Personal).Yield(); 140 143 141 144 /// <summary> 142 145 /// The main storage as proposed by the host game. ··· 187 190 188 191 private double maximumUpdateHz; 189 192 193 + /// <summary> 194 + /// The target number of update frames per second when the game window is active. 195 + /// </summary> 196 + /// <remarks> 197 + /// A value of 0 is treated the same as "unlimited" or <see cref="double.MaxValue"/>. 198 + /// </remarks> 190 199 public double MaximumUpdateHz 191 200 { 192 201 get => maximumUpdateHz; ··· 195 204 196 205 private double maximumDrawHz; 197 206 207 + /// <summary> 208 + /// The target number of draw frames per second when the game window is active. 209 + /// </summary> 210 + /// <remarks> 211 + /// A value of 0 is treated the same as "unlimited" or <see cref="double.MaxValue"/>. 212 + /// </remarks> 198 213 public double MaximumDrawHz 199 214 { 200 215 get => maximumDrawHz; 201 216 set => DrawThread.ActiveHz = maximumDrawHz = value; 202 217 } 203 218 219 + /// <summary> 220 + /// The target number of updates per second when the game window is inactive. 221 + /// This is applied to all threads. 222 + /// </summary> 223 + /// <remarks> 224 + /// A value of 0 is treated the same as "unlimited" or <see cref="double.MaxValue"/>. 225 + /// </remarks> 204 226 public double MaximumInactiveHz 205 227 { 206 228 get => DrawThread.InactiveHz; ··· 680 702 /// Finds the default <see cref="Storage"/> for the game to be used if <see cref="Game.CreateStorage"/> is not overridden. 681 703 /// </summary> 682 704 /// <returns>The <see cref="Storage"/>.</returns> 683 - protected virtual Storage GetDefaultGameStorage() => GetStorage(UserStoragePath).GetStorageForDirectory(Name); 705 + protected virtual Storage GetDefaultGameStorage() 706 + { 707 + // first check all valid paths for any existing install. 708 + foreach (var path in UserStoragePaths) 709 + { 710 + var storage = GetStorage(path); 711 + 712 + // if an existing data directory exists for this application, prefer it immediately. 713 + if (storage.ExistsDirectory(Name)) 714 + return storage.GetStorageForDirectory(Name); 715 + } 716 + 717 + // if an existing directory could not be found, use the first path that can be created. 718 + foreach (var path in UserStoragePaths) 719 + { 720 + try 721 + { 722 + return GetStorage(path).GetStorageForDirectory(Name); 723 + } 724 + catch 725 + { 726 + // may fail on directory creation. 727 + } 728 + } 729 + 730 + throw new InvalidOperationException("No valid user storage path could be resolved."); 731 + } 684 732 685 733 /// <summary> 686 734 /// Pauses all active threads. Call <see cref="Resume"/> to resume execution.
+17 -5
osu.Framework/Platform/HeadlessGameHost.cs
··· 26 26 27 27 public override void OpenUrlExternally(string url) => Logger.Log($"Application has requested URL \"{url}\" to be opened."); 28 28 29 - public override string UserStoragePath => "./headless/"; 29 + public override IEnumerable<string> UserStoragePaths => new[] { "./headless/" }; 30 30 31 31 public HeadlessGameHost(string gameName = null, bool bindIPC = false, bool realtime = true, bool portableInstallation = false) 32 32 : base(gameName ?? Guid.NewGuid().ToString(), bindIPC, portableInstallation: portableInstallation) ··· 51 51 { 52 52 base.SetupForRun(); 53 53 54 - MaximumDrawHz = double.MaxValue; 55 - MaximumUpdateHz = double.MaxValue; 56 - MaximumInactiveHz = double.MaxValue; 54 + // We want the draw thread to run, but it doesn't matter how fast it runs. 55 + // This limiting is mostly to reduce CPU overhead. 56 + MaximumDrawHz = 60; 57 57 58 - if (!realtime) customClock = new FramedClock(new FastClock(CLOCK_RATE)); 58 + if (!realtime) 59 + { 60 + customClock = new FramedClock(new FastClock(CLOCK_RATE)); 61 + 62 + // time is incremented per frame, rather than based on the real-world time. 63 + // therefore our goal is to run frames as fast as possible. 64 + MaximumUpdateHz = MaximumInactiveHz = 0; 65 + } 66 + else 67 + { 68 + // in realtime runs, set a sane upper limit to avoid cpu overhead from spinning. 69 + MaximumUpdateHz = MaximumInactiveHz = 1000; 70 + } 59 71 } 60 72 61 73 protected override void DrawFrame()
+8 -13
osu.Framework/Platform/Linux/LinuxGameHost.cs
··· 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 4 using System; 5 + using System.Collections.Generic; 5 6 using System.IO; 6 7 using osu.Framework.Platform.Linux.SDL2; 7 8 ··· 16 17 17 18 protected override IWindow CreateWindow() => new SDL2DesktopWindow(); 18 19 19 - public override string UserStoragePath 20 + public override IEnumerable<string> UserStoragePaths 20 21 { 21 22 get 22 23 { 23 - string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal); 24 24 string xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); 25 - string[] paths = 26 - { 27 - xdg ?? Path.Combine(home, ".local", "share"), 28 - Path.Combine(home) 29 - }; 25 + 26 + if (!string.IsNullOrEmpty(xdg)) 27 + yield return xdg; 30 28 31 - foreach (string path in paths) 32 - { 33 - if (Directory.Exists(path)) 34 - return path; 35 - } 29 + yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".local", "share"); 36 30 37 - return paths[0]; 31 + foreach (var path in base.UserStoragePaths) 32 + yield return path; 38 33 } 39 34 } 40 35
+7 -13
osu.Framework/Platform/MacOS/MacOSGameHost.cs
··· 22 22 23 23 protected override IWindow CreateWindow() => new MacOSWindow(); 24 24 25 - public override string UserStoragePath 25 + public override IEnumerable<string> UserStoragePaths 26 26 { 27 27 get 28 28 { 29 - string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal); 30 29 string xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); 31 - string[] paths = 32 - { 33 - xdg ?? Path.Combine(home, ".local", "share"), 34 - Path.Combine(home) 35 - }; 36 30 37 - foreach (string path in paths) 38 - { 39 - if (Directory.Exists(path)) 40 - return path; 41 - } 31 + if (!string.IsNullOrEmpty(xdg)) 32 + yield return xdg; 42 33 43 - return paths[0]; 34 + yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".local", "share"); 35 + 36 + foreach (var path in base.UserStoragePaths) 37 + yield return path; 44 38 } 45 39 } 46 40
+4 -1
osu.Framework/Platform/Windows/WindowsGameHost.cs
··· 6 6 using System.Diagnostics; 7 7 using System.IO; 8 8 using System.Linq; 9 + using osu.Framework.Extensions.IEnumerableExtensions; 9 10 using osu.Framework.Input; 10 11 using osu.Framework.Input.Bindings; 11 12 using osu.Framework.Input.Handlers; ··· 20 21 21 22 public override Clipboard GetClipboard() => new WindowsClipboard(); 22 23 23 - public override string UserStoragePath => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); 24 + public override IEnumerable<string> UserStoragePaths => 25 + // on windows this is guaranteed to exist (and be usable) so don't fallback to the base/default. 26 + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData).Yield(); 24 27 25 28 #if NET5_0 26 29 [System.Runtime.Versioning.SupportedOSPlatform("windows")]
+1 -1
osu.Framework/Statistics/PerformanceMonitor.cs
··· 57 57 58 58 private Thread thread; 59 59 60 - public double FrameAimTime => 1000.0 / (Clock?.MaximumUpdateHz ?? double.MaxValue); 60 + public double FrameAimTime => 1000.0 / (Clock?.MaximumUpdateHz > 0 ? Clock.MaximumUpdateHz : double.MaxValue); 61 61 62 62 internal PerformanceMonitor(GameThread thread, IEnumerable<StatisticsCounterType> counters) 63 63 {
-4
osu.Framework/Testing/TestBrowserTestRunner.cs
··· 41 41 { 42 42 base.LoadComplete(); 43 43 44 - host.MaximumDrawHz = int.MaxValue; 45 - host.MaximumUpdateHz = int.MaxValue; 46 - host.MaximumInactiveHz = int.MaxValue; 47 - 48 44 AddInternal(browser); 49 45 50 46 Console.WriteLine($@"{(int)Time.Current}: Running {browser.TestTypes.Count} visual test cases...");
+4 -2
osu.Framework/Testing/TestRunHeadlessGameHost.cs
··· 1 1 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 + using System.Collections.Generic; 4 5 using System.IO; 6 + using osu.Framework.Extensions.IEnumerableExtensions; 5 7 using osu.Framework.Logging; 6 8 using osu.Framework.Platform; 7 9 ··· 14 16 { 15 17 private readonly bool bypassCleanup; 16 18 17 - public override string UserStoragePath { get; } 19 + public override IEnumerable<string> UserStoragePaths { get; } 18 20 19 21 public static string TemporaryTestDirectory = Path.Combine(Path.GetTempPath(), "of-test-headless"); 20 22 ··· 22 24 : base(name, bindIPC, realtime, portableInstallation) 23 25 { 24 26 this.bypassCleanup = bypassCleanup; 25 - UserStoragePath = TemporaryTestDirectory; 27 + UserStoragePaths = TemporaryTestDirectory.Yield(); 26 28 } 27 29 28 30 protected override void Dispose(bool isDisposing)
-9
osu.Framework/Testing/TestSceneTestRunner.cs
··· 58 58 if (volume != null) volume.Value = volumeAtStartup; 59 59 } 60 60 61 - protected override void LoadComplete() 62 - { 63 - base.LoadComplete(); 64 - 65 - host.MaximumDrawHz = int.MaxValue; 66 - host.MaximumUpdateHz = int.MaxValue; 67 - host.MaximumInactiveHz = int.MaxValue; 68 - } 69 - 70 61 /// <summary> 71 62 /// Blocks execution until a provided <see cref="TestScene"/> runs to completion. 72 63 /// </summary>
+8 -2
osu.Framework/Threading/GameThread.cs
··· 97 97 private CultureInfo culture; 98 98 99 99 /// <summary> 100 - /// The refresh rate this thread should run at when active. Only applies when the underlying clock is throttling. 100 + /// The target number of updates per second when the game window is active. 101 101 /// </summary> 102 + /// <remarks> 103 + /// A value of 0 is treated the same as "unlimited" or <see cref="double.MaxValue"/>. 104 + /// </remarks> 102 105 public double ActiveHz 103 106 { 104 107 get => activeHz; ··· 112 115 private double activeHz = DEFAULT_ACTIVE_HZ; 113 116 114 117 /// <summary> 115 - /// The refresh rate this thread should run at when inactive. Only applies when the underlying clock is throttling. 118 + /// The target number of updates per second when the game window is inactive. 116 119 /// </summary> 120 + /// <remarks> 121 + /// A value of 0 is treated the same as "unlimited" or <see cref="double.MaxValue"/>. 122 + /// </remarks> 117 123 public double InactiveHz 118 124 { 119 125 get => inactiveHz;
+6 -3
osu.Framework/Timing/ThrottledFrameClock.cs
··· 15 15 public class ThrottledFrameClock : FramedClock 16 16 { 17 17 /// <summary> 18 - /// The number of updated per second which is permitted. 18 + /// The target number of updates per second. Only used when <see cref="Throttling"/> is <c>true</c>. 19 19 /// </summary> 20 + /// <remarks> 21 + /// A value of 0 is treated the same as "unlimited" or <see cref="double.MaxValue"/>. 22 + /// </remarks> 20 23 public double MaximumUpdateHz = 1000.0; 21 24 22 25 /// <summary> 23 - /// Whether throttling should be enabled. Defaults to true. 26 + /// Whether throttling should be enabled. Defaults to <c>true</c>. 24 27 /// </summary> 25 28 public bool Throttling = true; 26 29 ··· 37 40 38 41 if (Throttling) 39 42 { 40 - if (MaximumUpdateHz > 0) 43 + if (MaximumUpdateHz > 0 && MaximumUpdateHz < double.MaxValue) 41 44 { 42 45 throttle(); 43 46 }