diff --git a/osu.Framework.Tests/Visual/Platform/TestSceneRenderer.cs b/osu.Framework.Tests/Visual/Platform/TestSceneRenderer.cs new file mode 100644 index 0000000000..9d5a212dcc --- /dev/null +++ b/osu.Framework.Tests/Visual/Platform/TestSceneRenderer.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Platform; + +namespace osu.Framework.Tests.Visual.Platform +{ + [Ignore("This test cannot be run in headless mode (a renderer is required).")] + public partial class TestSceneRenderer : FrameworkTestScene + { + [Resolved] + private GameHost host { get; set; } = null!; + + [Resolved] + private FrameworkConfigManager config { get; set; } = null!; + + [SetUp] + public void SetUp() => Schedule(() => + { + Add(new SpriteText + { + Text = $"Renderer: {host.ResolvedRenderer} ({host.Renderer.GetType().Name} / {host.Window.GraphicsSurface.Type})", + Font = FrameworkFont.Regular.With(size: 24), + }); + + Add(new BasicDropdown + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Items = host.GetPreferredRenderersForCurrentPlatform().OrderBy(t => t), + Current = config.GetBindable(FrameworkSetting.Renderer), + Width = 200f, + }); + }); + } +} diff --git a/osu.Framework.iOS/IOSGameHost.cs b/osu.Framework.iOS/IOSGameHost.cs index efdd650bce..fae83e0251 100644 --- a/osu.Framework.iOS/IOSGameHost.cs +++ b/osu.Framework.iOS/IOSGameHost.cs @@ -38,7 +38,7 @@ public IOSGameHost(IOSGameView gameView) this.gameView = gameView; } - protected override IRenderer CreateRenderer() => new IOSGLRenderer(gameView); + protected override IRenderer CreateGLRenderer() => new IOSGLRenderer(gameView); protected override void SetupForRun() { diff --git a/osu.Framework/Configuration/FrameworkConfigManager.cs b/osu.Framework/Configuration/FrameworkConfigManager.cs index 1935204e02..c8498c53ae 100644 --- a/osu.Framework/Configuration/FrameworkConfigManager.cs +++ b/osu.Framework/Configuration/FrameworkConfigManager.cs @@ -39,6 +39,7 @@ protected override void InitialiseDefaults() SetDefault(FrameworkSetting.SizeFullscreen, new Size(9999, 9999), new Size(320, 240)); SetDefault(FrameworkSetting.FrameSync, FrameSync.Limit2x); SetDefault(FrameworkSetting.WindowMode, WindowMode.Windowed); + SetDefault(FrameworkSetting.Renderer, RendererType.Automatic); SetDefault(FrameworkSetting.ShowUnicode, false); SetDefault(FrameworkSetting.Locale, string.Empty); @@ -90,6 +91,7 @@ public enum FrameworkSetting SizeFullscreen, + Renderer, WindowMode, ConfineMouseMode, FrameSync, diff --git a/osu.Framework/Configuration/RendererType.cs b/osu.Framework/Configuration/RendererType.cs new file mode 100644 index 0000000000..bd028b074d --- /dev/null +++ b/osu.Framework/Configuration/RendererType.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; + +namespace osu.Framework.Configuration +{ + public enum RendererType + { + [Description("Automatic")] + Automatic, + + [Description("Metal")] + Metal, + + [Description("Vulkan")] + Vulkan, + + [Description("Direct3D 11")] + Direct3D11, + + [Description("OpenGL")] + OpenGL, + + [Description("OpenGL (Legacy)")] + OpenGLLegacy, + } +} diff --git a/osu.Framework/Platform/GameHost.cs b/osu.Framework/Platform/GameHost.cs index 5d5e679b60..1f52e06ec5 100644 --- a/osu.Framework/Platform/GameHost.cs +++ b/osu.Framework/Platform/GameHost.cs @@ -23,6 +23,7 @@ using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Development; +using osu.Framework.Extensions; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.TypeExtensions; @@ -250,7 +251,7 @@ public void UnregisterThread(GameThread thread) public InputThread InputThread { get; private set; } public AudioThread AudioThread { get; private set; } - private double maximumUpdateHz; + private double maximumUpdateHz = GameThread.DEFAULT_ACTIVE_HZ; /// /// The target number of update frames per second when the game window is active. @@ -264,7 +265,7 @@ public double MaximumUpdateHz set => threadRunner.MaximumUpdateHz = UpdateThread.ActiveHz = maximumUpdateHz = value; } - private double maximumDrawHz; + private double maximumDrawHz = GameThread.DEFAULT_ACTIVE_HZ; /// /// The target number of draw frames per second when the game window is active. @@ -275,9 +276,16 @@ public double MaximumUpdateHz public double MaximumDrawHz { get => maximumDrawHz; - set => DrawThread.ActiveHz = maximumDrawHz = value; + set + { + maximumDrawHz = value; + if (DrawThread != null) + DrawThread.ActiveHz = maximumDrawHz; + } } + private double maximumInactiveHz = GameThread.DEFAULT_INACTIVE_HZ; + /// /// The target number of updates per second when the game window is inactive. /// This is applied to all threads. @@ -287,11 +295,12 @@ public double MaximumDrawHz /// public double MaximumInactiveHz { - get => DrawThread.InactiveHz; + get => maximumInactiveHz; set { - DrawThread.InactiveHz = value; - threadRunner.MaximumInactiveHz = UpdateThread.InactiveHz = value; + threadRunner.MaximumInactiveHz = UpdateThread.InactiveHz = maximumInactiveHz = value; + if (DrawThread != null) + DrawThread.InactiveHz = maximumInactiveHz; } } @@ -326,19 +335,7 @@ protected GameHost([NotNull] string gameName, [CanBeNull] HostOptions options = }; } - protected virtual IRenderer CreateRenderer() - { - switch (FrameworkEnvironment.PreferredGraphicsRenderer) - { - case "veldrid": - return new VeldridRenderer(); - - default: - case "gl": - case "opengl": - return new GLRenderer(); - } - } + protected virtual IRenderer CreateGLRenderer() => new GLRenderer(); /// /// Performs a GC collection and frees all framework caches. @@ -680,8 +677,6 @@ public void Run(Game game) if (ExecutionState != ExecutionState.Idle) throw new InvalidOperationException("A game that has already been run cannot be restarted."); - Renderer = CreateRenderer(); - try { if (!host_running_mutex.Wait(10000)) @@ -701,13 +696,6 @@ public void Run(Game game) Monitor = { HandleGC = true }, }); - GraphicsSurfaceType surfaceType = FrameworkEnvironment.PreferredGraphicsSurface ?? GraphicsSurfaceType.OpenGL; - - Logger.Log("Using renderer: " + Renderer.GetType().ReadableName()); - Logger.Log("Using graphics surface: " + surfaceType); - - RegisterThread(DrawThread = new DrawThread(DrawFrame, this, $"{Renderer.GetType().ReadableName().Replace("Renderer", "")} / {surfaceType}")); - Trace.Listeners.Clear(); Trace.Listeners.Add(new ThrowingTraceListener()); @@ -718,34 +706,27 @@ public void Run(Game game) Dependencies.CacheAs(this); Dependencies.CacheAs(Storage = game.CreateStorage(this, GetDefaultGameStorage())); - Dependencies.CacheAs(Renderer); CacheStorage = GetDefaultGameStorage().GetStorageForDirectory("cache"); SetupForRun(); - Window = CreateWindow(surfaceType); - populateInputHandlers(); SetupConfig(game.GetFrameworkConfigDefaults() ?? new Dictionary()); - initialiseInputHandlers(); - - if (Window != null) - { - Window.SetupWindow(Config); - - Window.Create(); - Window.Title = $@"osu!framework (running ""{Name}"")"; + ChooseAndSetupRenderer(); - Renderer.Initialise(Window.GraphicsSurface); + initialiseInputHandlers(); - currentDisplayMode = Window.CurrentDisplayMode.GetBoundCopy(); - currentDisplayMode.BindValueChanged(_ => updateFrameSyncMode()); + // Prepare renderer (requires config). + Dependencies.CacheAs(Renderer); - IsActive.BindTo(Window.IsActive); - } + RegisterThread(DrawThread = new DrawThread(DrawFrame, this, $"{Renderer.GetType().ReadableName().Replace("Renderer", "")} / {(Window?.GraphicsSurface.Type.ToString() ?? "headless")}") + { + ActiveHz = MaximumDrawHz, + InactiveHz = MaximumInactiveHz, + }); Dependencies.CacheAs(readableKeyCombinationProvider = CreateReadableKeyCombinationProvider()); Dependencies.CacheAs(CreateTextInput()); @@ -814,6 +795,191 @@ public void Run(Game game) } } + /// + /// The renderer which the game host is currently running with. + /// + /// + /// This is similar to except that this is expressed as a rather than a . + /// + public RendererType ResolvedRenderer { get; private set; } + + /// + /// All valid s for the current platform, in order of how stable and performant they are deemed to be. + /// + public IEnumerable GetPreferredRenderersForCurrentPlatform() + { + yield return RendererType.Automatic; + + // Preferred per-platform renderers + switch (RuntimeInfo.OS) + { + case RuntimeInfo.Platform.Windows: + yield return RendererType.Direct3D11; + + break; + + case RuntimeInfo.Platform.macOS: + case RuntimeInfo.Platform.iOS: + yield return RendererType.Metal; + + break; + } + + // Non-veldrid "known-to-work". + yield return RendererType.OpenGLLegacy; + + // Other available renderers should also be returned (to make this method usable as "all available renderers for current platform"), + // but will never be preferred as OpenGLLegacy will always work. + yield return RendererType.OpenGL; + + if (!RuntimeInfo.IsApple) yield return RendererType.Vulkan; + } + + protected virtual void ChooseAndSetupRenderer() + { + // Always give preference to environment variables. + if (FrameworkEnvironment.PreferredGraphicsSurface != null || FrameworkEnvironment.PreferredGraphicsRenderer != null) + { + Logger.Log("🖼️ Using environment variables for renderer and surface selection.", level: LogLevel.Important); + + // And allow this to hard fail with no fallbacks. + SetupRendererAndWindow( + FrameworkEnvironment.PreferredGraphicsRenderer ?? "veldrid", + FrameworkEnvironment.PreferredGraphicsSurface ?? GraphicsSurfaceType.OpenGL); + return; + } + + var configRenderer = Config.GetBindable(FrameworkSetting.Renderer); + Logger.Log($"🖼️ Configuration renderer choice: {configRenderer}"); + + // Attempt to initialise various veldrid surface types (and legacy GL). + var rendererTypes = GetPreferredRenderersForCurrentPlatform().Where(r => r != RendererType.Automatic).ToList(); + + // Move user's preference to the start of the attempts. + if (!configRenderer.IsDefault) + { + rendererTypes.Remove(configRenderer.Value); + rendererTypes.Insert(0, configRenderer.Value); + } + + Logger.Log($"🖼️ Renderer fallback order: [ {string.Join(", ", rendererTypes.Select(e => e.GetDescription()))} ]"); + + foreach (RendererType type in rendererTypes) + { + try + { + if (type == RendererType.OpenGLLegacy) + // the legacy renderer. this is basically guaranteed to support all platforms. + SetupRendererAndWindow("gl", GraphicsSurfaceType.OpenGL); + else + SetupRendererAndWindow("veldrid", rendererToGraphicsSurfaceType(type)); + + ResolvedRenderer = type; + return; + } + catch + { + if (configRenderer.Value != RendererType.Automatic) + { + // If we fail, assume the user may have had a custom setting and switch it back to automatic. + Logger.Log($"The selected renderer ({configRenderer.Value.GetDescription()}) failed to initialise. Renderer selection has been reverted to automatic.", + level: LogLevel.Important); + configRenderer.Value = RendererType.Automatic; + } + } + } + + Logger.Log("No usable renderer was found!", level: LogLevel.Error); + } + + private static GraphicsSurfaceType rendererToGraphicsSurfaceType(RendererType renderer) + { + GraphicsSurfaceType surface; + + switch (renderer) + { + case RendererType.Metal: + surface = GraphicsSurfaceType.Metal; + break; + + case RendererType.Vulkan: + surface = GraphicsSurfaceType.Vulkan; + break; + + case RendererType.Direct3D11: + surface = GraphicsSurfaceType.Direct3D11; + break; + + case RendererType.OpenGL: + surface = GraphicsSurfaceType.OpenGL; + break; + + default: + throw new ArgumentException("Provided renderer cannot be mapped to a veldrid surface"); + } + + return surface; + } + + protected void SetupRendererAndWindow(string renderer, GraphicsSurfaceType surfaceType) + { + switch (renderer) + { + case "veldrid": + SetupRendererAndWindow(new VeldridRenderer(), surfaceType); + break; + + default: + case "gl": + SetupRendererAndWindow(CreateGLRenderer(), surfaceType); + break; + } + } + + protected void SetupRendererAndWindow(IRenderer renderer, GraphicsSurfaceType surfaceType) + { + Logger.Log($"🖼️ Initialising \"{renderer.GetType().ReadableName().Replace("Renderer", "")}\" renderer with \"{surfaceType}\" surface"); + + Renderer = renderer; + + // Prepare window + Window = CreateWindow(surfaceType); + + if (Window == null) + { + Logger.Log("🖼️ Renderer could not be initialised, no window exists."); + return; + } + + try + { + Window.SetupWindow(Config); + Window.Create(); + Window.Title = $@"osu!framework (running ""{Name}"")"; + + Renderer.Initialise(Window.GraphicsSurface); + + Logger.Log("🖼️ Renderer initialised!"); + } + catch (Exception e) + { + Logger.Log("🖼️ Renderer initialisation failed with:"); + Logger.Log(e.ToString()); + + Window?.Close(); + Window?.Dispose(); + Window = null; + + Renderer = null; + throw; + } + + currentDisplayMode = Window.CurrentDisplayMode.GetBoundCopy(); + currentDisplayMode.BindValueChanged(_ => updateFrameSyncMode()); + + IsActive.BindTo(Window.IsActive); + } + /// /// Finds the default for the game to be used if is not overridden. /// diff --git a/osu.Framework/Platform/HeadlessGameHost.cs b/osu.Framework/Platform/HeadlessGameHost.cs index 560b593fa5..0dbc7a5d6c 100644 --- a/osu.Framework/Platform/HeadlessGameHost.cs +++ b/osu.Framework/Platform/HeadlessGameHost.cs @@ -46,6 +46,8 @@ public HeadlessGameHost(string gameName = null, HostOptions options = null, bool this.realtime = realtime; } + protected override void ChooseAndSetupRenderer() => SetupRendererAndWindow("gl", GraphicsSurfaceType.OpenGL); + protected override void SetupConfig(IDictionary defaultOverrides) { defaultOverrides[FrameworkSetting.AudioDevice] = "No sound"; diff --git a/osu.Framework/Platform/OsuTKGameHost.cs b/osu.Framework/Platform/OsuTKGameHost.cs index 4c8d835d08..d0a599ff4c 100644 --- a/osu.Framework/Platform/OsuTKGameHost.cs +++ b/osu.Framework/Platform/OsuTKGameHost.cs @@ -18,7 +18,7 @@ protected OsuTKGameHost() toolkit = Toolkit.Init(); } - protected override IRenderer CreateRenderer() => new GLRenderer(); + protected override IRenderer CreateGLRenderer() => new GLRenderer(); protected override void Dispose(bool disposing) { diff --git a/osu.Framework/Platform/SDL2DesktopWindow.cs b/osu.Framework/Platform/SDL2DesktopWindow.cs index 18f167ddf4..ab0abdaf36 100644 --- a/osu.Framework/Platform/SDL2DesktopWindow.cs +++ b/osu.Framework/Platform/SDL2DesktopWindow.cs @@ -232,11 +232,10 @@ public virtual void Create() if (SDLWindowHandle == IntPtr.Zero) throw new InvalidOperationException($"Failed to create SDL window. SDL Error: {SDL.SDL_GetError()}"); - Exists = true; - graphicsSurface.Initialise(); initialiseWindowingAfterCreation(); + Exists = true; } /// @@ -281,11 +280,10 @@ public void Run() Update?.Invoke(); } + Exists = false; Exited?.Invoke(); - if (SDLWindowHandle != IntPtr.Zero) - SDL.SDL_DestroyWindow(SDLWindowHandle); - + Close(); SDL.SDL_Quit(); } @@ -303,7 +301,22 @@ public void OnDraw() /// /// Forcefully closes the window. /// - public void Close() => ScheduleCommand(() => Exists = false); + public void Close() + { + if (Exists) + { + // Close will be called as part of finishing the Run loop. + ScheduleCommand(() => Exists = false); + } + else + { + if (SDLWindowHandle != IntPtr.Zero) + { + SDL.SDL_DestroyWindow(SDLWindowHandle); + SDLWindowHandle = IntPtr.Zero; + } + } + } public void Raise() => ScheduleCommand(() => { @@ -530,6 +543,8 @@ internal virtual void SetIconFromGroup(IconGroup iconGroup) public void Dispose() { + Close(); + SDL.SDL_Quit(); } } } diff --git a/osu.Framework/Platform/Windows/WindowsGameHost.cs b/osu.Framework/Platform/Windows/WindowsGameHost.cs index 18af9a9bad..8ef2cdbbad 100644 --- a/osu.Framework/Platform/Windows/WindowsGameHost.cs +++ b/osu.Framework/Platform/Windows/WindowsGameHost.cs @@ -65,13 +65,7 @@ protected override IEnumerable CreateAvailableInputHandlers() .Concat(new InputHandler[] { new WindowsMouseHandler() }); } - protected override IRenderer CreateRenderer() - { - if (FrameworkEnvironment.PreferredGraphicsRenderer != null || FrameworkEnvironment.PreferredGraphicsSurface != null) - return base.CreateRenderer(); - - return new WindowsGLRenderer(this); - } + protected override IRenderer CreateGLRenderer() => new WindowsGLRenderer(this); protected override void SetupForRun() {