Files
osu-framework/osu.Framework/Platform/GameHost.cs
2026-01-16 12:28:33 +08:00

1631 lines
66 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osuTK;
using osu.Framework.Allocation;
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;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Deferred;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Handlers;
using osu.Framework.Logging;
using osu.Framework.Statistics;
using osu.Framework.Threading;
using osu.Framework.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Veldrid;
using osu.Framework.Graphics.Video;
using osu.Framework.IO.Serialization;
using osu.Framework.IO.Stores;
using osu.Framework.Localisation;
using Rectangle = System.Drawing.Rectangle;
using Size = System.Drawing.Size;
namespace osu.Framework.Platform
{
public abstract class GameHost : IIpcHost, IDisposable
{
public IWindow Window { get; private set; }
/// <summary>
/// Whether <see cref="Window"/> needs to be non-null for startup to succeed.
/// </summary>
protected virtual bool RequireWindowExists => true;
public IRenderer Renderer { get; private set; }
public string RendererInfo { get; private set; }
/// <summary>
/// Whether "unlimited" frame limiter should be allowed to exceed sane limits.
/// Only use this for benchmarking purposes (see <see cref="maximum_sane_fps"/> for further reasoning).
/// </summary>
public bool AllowBenchmarkUnlimitedFrames { get; set; }
private int? originalMaxTexturesUploadedPerFrame;
private int? originalMaxPixelsUploadedPerFrame;
protected FrameworkDebugConfigManager DebugConfig { get; private set; }
protected FrameworkConfigManager Config { get; private set; }
private InputConfigManager inputConfig { get; set; }
/// <summary>
/// Whether the <see cref="IWindow"/> is active (in the foreground).
/// </summary>
public readonly IBindable<bool> IsActive = new Bindable<bool>(true);
/// <summary>
/// Disable any system level timers that might dim or turn off the screen.
/// </summary>
/// <remarks>
/// To preserve battery life on mobile devices, this should be left on whenever possible.
/// </remarks>
public readonly AggregateBindable<bool> AllowScreenSuspension = new AggregateBindable<bool>((a, b) => a & b, new Bindable<bool>(true));
/// <summary>
/// For IPC messaging purposes, whether this <see cref="GameHost"/> is the primary (bound) host.
/// </summary>
public virtual bool IsPrimaryInstance { get; protected set; } = true;
/// <summary>
/// Invoked when the game window is activated. Always invoked from the update thread.
/// </summary>
public event Action Activated;
/// <summary>
/// Invoked when the game window is deactivated. Always invoked from the update thread.
/// </summary>
public event Action Deactivated;
/// <summary>
/// Invoked when an exit was requested. Always invoked from the update thread.
/// </summary>
/// <remarks>
/// Usually invoked when the window close (X) button or another platform-native exit action has been pressed.
/// </remarks>
public event Action ExitRequested;
public event Action Exited;
/// <summary>
/// An unhandled exception was thrown. Return true to ignore and continue running.
/// </summary>
public event Func<Exception, bool> ExceptionThrown;
[CanBeNull]
public event Func<IpcMessage, IpcMessage> MessageReceived;
/// <summary>
/// Whether the on-screen keyboard covers a portion of the game window when presented to the user.
/// </summary>
public virtual bool OnScreenKeyboardOverlapsGameWindow => false;
/// <summary>
/// Whether this host can exit (mobile platforms, for instance, do not support exiting the app).
/// </summary>
/// <remarks>Also see <see cref="CanSuspendToBackground"/>.</remarks>
public virtual bool CanExit => true;
/// <summary>
/// Whether this host can suspend and minimize to background.
/// </summary>
/// <remarks>
/// This and <see cref="SuspendToBackground"/> are an alternative way to exit on hosts that have <see cref="CanExit"/> <c>false</c>.
/// </remarks>
public virtual bool CanSuspendToBackground => false;
protected IpcMessage OnMessageReceived(IpcMessage message) => MessageReceived?.Invoke(message);
public virtual Task SendMessageAsync(IpcMessage message) => throw new NotSupportedException("This platform does not implement IPC.");
public virtual Task<IpcMessage> SendMessageWithResponseAsync(IpcMessage message) => throw new NotSupportedException("This platform does not implement IPC.");
/// <summary>
/// Requests that a file or folder be opened externally with an associated application, if available.
/// </summary>
/// <remarks>
/// Some platforms do not support interacting with files externally (ie. mobile or sandboxed platforms), check the return value to discern whether it succeeded.
/// </remarks>
/// <param name="filename">The absolute path to the file which should be opened.</param>
/// <returns>Whether the file was successfully opened.</returns>
public abstract bool OpenFileExternally(string filename);
/// <summary>
/// Requests to present a file externally in the platform's native file browser.
/// </summary>
/// <remarks>
/// This will open the parent folder and, (if available) highlight the file.
/// Some platforms do not support interacting with files externally (ie. mobile or sandboxed platforms), check the return value to discern whether it succeeded.
///
/// If a folder path is provided to this method, it will prefer highlighting the folder in the parent folder, rather than showing the contents.
/// To display the contents of a folder, use <see cref="OpenFileExternally"/> instead.
/// </remarks>
/// <example>
/// <para>"C:\Windows\explorer.exe" -> opens 'C:\Windows' and highlights 'explorer.exe' in the window.</para>
/// <para>"C:\Windows\System32" -> opens 'C:\Windows' and highlights 'System32' in the window.</para>
/// </example>
/// <param name="filename">The absolute path to the file/folder to be shown in its parent folder.</param>
/// <returns>Whether the file was successfully presented.</returns>
public abstract bool PresentFileExternally(string filename);
/// <summary>
/// Requests that a URL be opened externally in a web browser, if available.
/// </summary>
/// <param name="url">The URL of the page which should be opened.</param>
public abstract void OpenUrlExternally(string url);
/// <summary>
/// Creates the game window for the host.
/// </summary>
protected abstract IWindow CreateWindow(GraphicsSurfaceType preferredSurface);
protected abstract Clipboard CreateClipboard();
protected virtual ReadableKeyCombinationProvider CreateReadableKeyCombinationProvider() => new ReadableKeyCombinationProvider();
private ReadableKeyCombinationProvider readableKeyCombinationProvider;
/// <summary>
/// The default initial path when requesting a user to select a file/folder.
/// </summary>
/// <remarks>
/// Provides a sane starting point for user-accessible storage.
/// </remarks>
public virtual string InitialFileSelectorPath => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
/// <summary>
/// Creates a provider component for interacting with a system-provided file selector.
/// </summary>
/// <param name="allowedExtensions">The list of extensions allowed to be selected, or empty to allow all files.</param>
[CanBeNull]
public virtual ISystemFileSelector CreateSystemFileSelector(string[] allowedExtensions) => null;
/// <summary>
/// Retrieve a storage for the specified location.
/// </summary>
/// <param name="path">The absolute path to be used as a root for the storage.</param>
public abstract Storage GetStorage(string path);
/// <summary>
/// All valid user storage paths in order of usage priority.
/// </summary>
public virtual IEnumerable<string> UserStoragePaths
// This is common to _most_ operating systems, with some specific ones overriding this value where a better option exists.
=> Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create).Yield();
/// <summary>
/// The main storage as proposed by the host game.
/// </summary>
public Storage Storage { get; protected set; }
/// <summary>
/// An auxiliary cache storage which is fixed in the default game directory.
/// </summary>
public Storage CacheStorage { get; protected set; }
/// <summary>
/// If caps-lock is enabled on the system, false if not overwritten by a subclass
/// </summary>
public virtual bool CapsLockEnabled => false;
public IEnumerable<GameThread> Threads => threadRunner.Threads;
/// <summary>
/// Register a thread to be monitored and tracked by this <see cref="GameHost"/>
/// </summary>
/// <param name="thread">The thread.</param>
public void RegisterThread(GameThread thread)
{
threadRunner.AddThread(thread);
thread.IsActive.BindTo(IsActive);
thread.UnhandledException = unhandledExceptionHandler;
if (thread.Monitor != null)
thread.Monitor.EnablePerformanceProfiling = PerformanceLogging.Value;
}
/// <summary>
/// Unregister a previously registered thread.<see cref="GameHost"/>
/// </summary>
/// <param name="thread">The thread.</param>
public void UnregisterThread(GameThread thread)
{
threadRunner.RemoveThread(thread);
IsActive.UnbindFrom(thread.IsActive);
thread.UnhandledException = null;
}
public DrawThread DrawThread { get; private set; }
public GameThread UpdateThread { get; private set; }
public InputThread InputThread { get; private set; }
public AudioThread AudioThread { get; private set; }
private double maximumUpdateHz = GameThread.DEFAULT_ACTIVE_HZ;
/// <summary>
/// The target number of update frames per second when the game window is active.
/// </summary>
/// <remarks>
/// A value of 0 is treated the same as "unlimited" or <see cref="double.MaxValue"/>.
/// </remarks>
public double MaximumUpdateHz
{
get => maximumUpdateHz;
set => threadRunner.MaximumUpdateHz = UpdateThread.ActiveHz = maximumUpdateHz = value;
}
private double maximumDrawHz = GameThread.DEFAULT_ACTIVE_HZ;
/// <summary>
/// The target number of draw frames per second when the game window is active.
/// </summary>
/// <remarks>
/// A value of 0 is treated the same as "unlimited" or <see cref="double.MaxValue"/>.
/// </remarks>
public double MaximumDrawHz
{
get => maximumDrawHz;
set
{
maximumDrawHz = value;
if (DrawThread != null)
DrawThread.ActiveHz = maximumDrawHz;
}
}
private double maximumInactiveHz = GameThread.DEFAULT_INACTIVE_HZ;
/// <summary>
/// The target number of updates per second when the game window is inactive.
/// This is applied to all threads.
/// </summary>
/// <remarks>
/// A value of 0 is treated the same as "unlimited" or <see cref="double.MaxValue"/>.
/// </remarks>
public double MaximumInactiveHz
{
get => maximumInactiveHz;
set
{
threadRunner.MaximumInactiveHz = UpdateThread.InactiveHz = maximumInactiveHz = value;
if (DrawThread != null)
DrawThread.InactiveHz = maximumInactiveHz;
}
}
private PerformanceMonitor inputMonitor => InputThread.Monitor;
private PerformanceMonitor drawMonitor => DrawThread.Monitor;
private readonly Lazy<string> fullPathBacking = new Lazy<string>(RuntimeInfo.GetFrameworkAssemblyPath);
public string FullPath => fullPathBacking.Value;
/// <summary>
/// The name of the game to be hosted.
/// </summary>
public string Name { get; }
[NotNull]
public HostOptions Options { get; private set; }
public DependencyContainer Dependencies { get; } = new DependencyContainer();
private bool suspended;
protected GameHost([NotNull] string gameName, [CanBeNull] HostOptions options = null)
{
Options = options ?? new HostOptions();
if (string.IsNullOrEmpty(Options.FriendlyGameName))
{
Options.FriendlyGameName = $@"osu!framework (running ""{gameName}"")";
}
Name = gameName;
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Converters = new List<JsonConverter> { new Vector2Converter() }
};
}
protected virtual IRenderer CreateGLRenderer() => new GLRenderer();
/// <summary>
/// Performs a GC collection and frees all framework caches.
/// This is a blocking call and should not be invoked during periods of user activity unless memory is critical.
/// </summary>
public void Collect()
{
SixLabors.ImageSharp.Configuration.Default.MemoryAllocator.ReleaseRetainedResources();
GC.Collect();
}
private void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs args)
{
var exception = (Exception)args.ExceptionObject;
logException(exception, "unhandled");
abortExecutionFromException(sender, exception, args.IsTerminating);
}
private void unobservedExceptionHandler(object sender, UnobservedTaskExceptionEventArgs args)
{
var actualException = args.Exception.AsSingular();
// unobserved exceptions are logged but left unhandled (most of the time they are not intended to be critical).
logException(actualException, "unobserved");
if (DebugUtils.IsNUnitRunning)
abortExecutionFromException(sender, actualException, false);
}
private void logException(Exception exception, string type)
{
Logger.Error(exception, $"An {type} error has occurred.", recursive: true);
}
/// <summary>
/// Give the running application a last change to handle an otherwise unhandled exception, and potentially ignore it.
/// </summary>
/// <param name="sender">The source, generally a <see cref="GameThread"/>.</param>
/// <param name="exception">The unhandled exception.</param>
/// <param name="isTerminating">Whether the CLR is terminating.</param>
private void abortExecutionFromException(object sender, Exception exception, bool isTerminating)
{
// nothing needs to be done if the consumer has requested continuing execution.
if (ExceptionThrown?.Invoke(exception) == true) return;
// otherwise, we need to unwind and abort execution.
AppDomain.CurrentDomain.UnhandledException -= unhandledExceptionHandler;
TaskScheduler.UnobservedTaskException -= unobservedExceptionHandler;
// In the case of an unhandled exception, it's feasible that the disposal flow for `GameHost` doesn't run.
// This can result in the exception not being logged (or being partially logged) due to the logger running asynchronously.
// We force flushing the logger here to ensure logging completes (and also unbind in the process since we're aborting execution from here).
Logger.FlushForShutdown();
var captured = ExceptionDispatchInfo.Capture(exception);
var thrownEvent = new ManualResetEventSlim(false);
//we want to throw this exception on the input thread to interrupt window and also headless execution.
InputThread.Scheduler.Add(() =>
{
try
{
captured.Throw();
}
finally
{
thrownEvent.Set();
}
});
// Stopping running threads before the exception is rethrown on the input thread causes some debuggers (e.g. Rider 2020.2) to not properly display the stack.
// To avoid this, pause the exceptioning thread until the rethrow takes place.
waitForThrow();
void waitForThrow()
{
// This is bypassed for sources in a few situations where deadlocks can occur:
// 1. When the exceptioning thread is GameThread.Input.
// 2. When the game is running in single-threaded mode. Single threaded stacks will be displayed correctly at the point of rethrow.
// 3. When the CLR is terminating. We can't guarantee the input thread is still running, and may delay application termination.
if (isTerminating || (sender is GameThread && (sender == InputThread || executionMode.Value == ExecutionMode.SingleThread)))
return;
// The process can deadlock in an extreme case such as the input thread dying before the delegate executes, so wait up to a maximum of 10 seconds at all times.
thrownEvent.Wait(TimeSpan.FromSeconds(10));
}
}
protected virtual void OnActivated() => UpdateThread.Scheduler.Add(() => Activated?.Invoke());
protected virtual void OnDeactivated() => UpdateThread.Scheduler.Add(() => Deactivated?.Invoke());
protected void OnExitRequested() => UpdateThread.Scheduler.Add(() => ExitRequested?.Invoke());
protected virtual void OnExited()
{
Exited?.Invoke();
}
private readonly TripleBuffer<DrawNode> drawRoots = new TripleBuffer<DrawNode>();
internal Container Root { get; private set; }
private ulong frameCount;
protected virtual void UpdateFrame()
{
if (Root == null) return;
frameCount++;
if (Window == null)
{
var windowedSize = Config.Get<Size>(FrameworkSetting.WindowedSize);
Root.Size = new Vector2(windowedSize.Width, windowedSize.Height);
}
else if (Window.WindowState != WindowState.Minimised)
Root.Size = new Vector2(Window.ClientSize.Width, Window.ClientSize.Height);
// Ensure we maintain a valid size for any children immediately scaling by the window size
Root.Size = Vector2.ComponentMax(Vector2.One, Root.Size);
TypePerformanceMonitor.NewFrame();
Root.UpdateSubTree();
Root.UpdateSubTreeMasking();
using (var buffer = drawRoots.GetForWrite())
buffer.Object = Root.GenerateDrawNodeSubtree(frameCount, buffer.Index, false);
}
private bool didRenderFrame;
protected virtual void DrawFrame()
{
Debug.Assert(Window != null);
if (Root == null)
return;
if (ExecutionState != ExecutionState.Running)
return;
if (Window.WindowState == WindowState.Minimised)
return;
Renderer.AllowTearing = windowMode.Value == WindowMode.Fullscreen;
TripleBuffer<DrawNode>.Buffer buffer;
using (drawMonitor.BeginCollecting(PerformanceCollectionType.Sleep))
{
// Importantly, only wait on renderer frame availability if we actually rendered a frame since the last `WaitUntilNextFrameReady()`.
// Without this, the wait handle, internally used in the Veldrid-side implementation of `WaitUntilNextFrameReady()`,
// will potentially be in a bad state and take the timeout value (1 second) to recover.
if (didRenderFrame)
Renderer.WaitUntilNextFrameReady();
didRenderFrame = false;
buffer = drawRoots.GetForRead(IsActive.Value ? TripleBuffer<DrawNode>.DEFAULT_READ_TIMEOUT : 0);
}
if (buffer == null)
return;
Debug.Assert(buffer.Object != null);
try
{
using (drawMonitor.BeginCollecting(PerformanceCollectionType.DrawReset))
Renderer.BeginFrame(new Vector2(Window.ClientSize.Width, Window.ClientSize.Height));
if (!bypassFrontToBackPass.Value)
{
Renderer.SetBlend(BlendingParameters.None);
Renderer.SetBlendMask(BlendingMask.None);
Renderer.PushDepthInfo(DepthInfo.Default);
// Front pass
DrawNode.DrawOtherOpaqueInterior(buffer.Object, Renderer);
Renderer.PopDepthInfo();
Renderer.SetBlendMask(BlendingMask.All);
// The back pass doesn't write depth, but needs to depth test properly
Renderer.PushDepthInfo(new DepthInfo(true, false));
}
else
{
// Disable depth testing
Renderer.PushDepthInfo(new DepthInfo(false, false));
}
// Back pass
DrawNode.DrawOther(buffer.Object, Renderer);
Renderer.PopDepthInfo();
Renderer.FinishFrame();
using (drawMonitor.BeginCollecting(PerformanceCollectionType.SwapBuffer))
Swap();
Window.OnDraw();
didRenderFrame = true;
}
finally
{
buffer.Dispose();
}
}
/// <summary>
/// Swap the buffers.
/// </summary>
protected virtual void Swap()
{
Renderer.SwapBuffers();
if (Window.GraphicsSurface.Type == GraphicsSurfaceType.OpenGL && Renderer.VerticalSync)
// without waiting (i.e. glFinish), vsync is basically unplayable due to the extra latency introduced.
// we will likely want to give the user control over this in the future as an advanced setting.
Renderer.WaitUntilIdle();
}
/// <summary>
/// Takes a screenshot of the game. The returned <see cref="Image{TPixel}"/> must be disposed by the caller when applicable.
/// </summary>
/// <returns>The screenshot as an <see cref="Image{TPixel}"/>.</returns>
public async Task<Image<Rgba32>> TakeScreenshotAsync()
{
if (Window == null) throw new InvalidOperationException($"{nameof(Window)} has not been set!");
using (var completionEvent = new ManualResetEventSlim(false))
{
Image<Rgba32> image = null;
DrawThread.Scheduler.Add(() =>
{
image = Renderer.TakeScreenshot();
// ReSharper disable once AccessToDisposedClosure
completionEvent.Set();
});
// this is required as attempting to use a TaskCompletionSource blocks the thread calling SetResult on some configurations.
// ReSharper disable once AccessToDisposedClosure
if (!await Task.Run(() => completionEvent.Wait(5000)).ConfigureAwait(false))
throw new TimeoutException("Screenshot data did not arrive in a timely fashion");
return image;
}
}
public ExecutionState ExecutionState
{
get => executionState;
private set
{
if (executionState == value)
return;
executionState = value;
Logger.Log($"Host execution state changed to {value}");
}
}
private ExecutionState executionState;
/// <summary>
/// Schedules the game to exit in the next frame.
/// </summary>
/// <remarks>Consider using <see cref="SuspendToBackground"/> on mobile platforms that can't exit normally.</remarks>
public void Exit()
{
if (CanExit)
PerformExit(false);
}
/// <summary>
/// Suspends and minimizes the game to background.
/// </summary>
/// <remarks>
/// This is provided as an alternative to <see cref="Exit"/> on hosts that can't exit (see <see cref="CanExit"/>).
/// Should only be called if <see cref="CanSuspendToBackground"/> is <c>true</c>.
/// </remarks>
/// <returns><c>true</c> if the game was successfully suspended and minimized.</returns>
public virtual bool SuspendToBackground()
{
return false;
}
/// <summary>
/// Schedules the game to exit in the next frame (or immediately if <paramref name="immediately"/> is true).
/// </summary>
/// <remarks>
/// Will never be called if <see cref="CanExit"/> is <see langword="false"/>.
/// </remarks>
/// <param name="immediately">If true, exits the game immediately. If false (default), schedules the game to exit in the next frame.</param>
protected virtual void PerformExit(bool immediately) => performExit(immediately);
private void performExit(bool immediately)
{
if (executionState == ExecutionState.Stopped || executionState == ExecutionState.Idle)
return;
ExecutionState = ExecutionState.Stopping;
if (immediately)
exit();
else
InputThread.Scheduler.Add(exit, false);
void exit()
{
Debug.Assert(ExecutionState == ExecutionState.Stopping);
Window?.Close();
threadRunner.Stop();
ExecutionState = ExecutionState.Stopped;
stoppedEvent.Set();
}
}
private static readonly SemaphoreSlim host_running_mutex = new SemaphoreSlim(1);
public void Run(Game game)
{
if (Thread.CurrentThread.IsThreadPoolThread)
{
// This is a common misuse of GameHost, where typically consumers will have a mutex waiting for the game to run.
// Exceptions thrown here will become unobserved, so any such mutexes will never be set.
// Instead, immediately terminate the application in order to notify of incorrect use in all cases.
Environment.FailFast($"{nameof(GameHost)}s should not be run on a TPL thread (use TaskCreationOptions.LongRunning).");
}
if (RuntimeInfo.IsDesktop)
{
// Mono (netcore) throws for this property
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
}
if (ExecutionState != ExecutionState.Idle)
throw new InvalidOperationException("A game that has already been run cannot be restarted.");
try
{
if (!host_running_mutex.Wait(10000))
throw new TimeoutException($"This {nameof(GameHost)} could not start {game} because another {nameof(GameHost)} was already running.");
threadRunner = CreateThreadRunner(InputThread = new InputThread());
AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler;
TaskScheduler.UnobservedTaskException += unobservedExceptionHandler;
RegisterThread(InputThread);
RegisterThread(AudioThread = new AudioThread());
RegisterThread(UpdateThread = new UpdateThread(UpdateFrame, DrawThread)
{
Monitor = { HandleGC = true },
});
Trace.Listeners.Clear();
Trace.Listeners.Add(new ThrowingTraceListener());
Logger.GameIdentifier = Name;
Logger.VersionIdentifier = RuntimeInfo.EntryAssembly.GetName().Version?.ToString() ?? Logger.VersionIdentifier;
Dependencies.CacheAs(this);
Dependencies.CacheAs(Storage = game.CreateStorage(this, GetDefaultGameStorage()));
CacheStorage = GetDefaultGameStorage().GetStorageForDirectory("cache");
SetupForRun();
game.SetupLogging(Storage, CacheStorage);
populateInputHandlers();
SetupConfig(game.GetFrameworkConfigDefaults() ?? new Dictionary<FrameworkSetting, object>());
ChooseAndSetupRenderer();
// Window creation may fail in the case of a catastrophic failure (ie. graphics driver or SDL3 level).
// In such cases, we want to throw here to immediately mark this renderer setup as failed.
if (RequireWindowExists && Window == null)
{
Logger.Log("Aborting startup as no window could be created.");
return;
}
initialiseInputHandlers();
// Prepare renderer (requires config).
Dependencies.CacheAs(Renderer);
RendererInfo = $"{Renderer.GetType().ReadableName().Replace("Renderer", "")} / {(Window?.GraphicsSurface.Type.ToString() ?? "headless")}";
RegisterThread(DrawThread = new DrawThread(DrawFrame, this)
{
ActiveHz = MaximumDrawHz,
InactiveHz = MaximumInactiveHz,
});
Dependencies.CacheAs(readableKeyCombinationProvider = CreateReadableKeyCombinationProvider());
Dependencies.CacheAs(CreateTextInput());
Dependencies.CacheAs(CreateClipboard());
ExecutionState = ExecutionState.Running;
threadRunner.Start();
DrawThread.WaitUntilInitialized();
bootstrapSceneGraph(game);
frameSyncMode.TriggerChange();
IsActive.BindValueChanged(active =>
{
if (active.NewValue)
OnActivated();
else
OnDeactivated();
}, true);
try
{
if (Window != null)
{
Window.Update += windowUpdate;
Window.Suspended += Suspend;
Window.Resumed += Resume;
Window.LowOnMemory += Collect;
Window.ExitRequested += OnExitRequested;
Window.Exited += OnExited;
Window.KeymapChanged += readableKeyCombinationProvider.OnKeymapChanged;
//we need to ensure all threads have stopped before the window is closed (mainly the draw thread
//to avoid GL operations running post-cleanup).
Window.Exited += threadRunner.Stop;
Window.Run();
}
else
{
while (ExecutionState != ExecutionState.Stopped)
windowUpdate();
}
}
catch (OutOfMemoryException)
{
}
}
finally
{
if (CanExit)
{
// Close the window and stop all threads
performExit(true);
host_running_mutex.Release();
}
}
}
/// <summary>
/// The renderer which the game host is currently running with.
/// </summary>
/// <remarks>
/// This is similar to <see cref="IGraphicsSurface.Type"/> except that this is expressed as a <see cref="RendererType"/> rather than a <see cref="GraphicsSurfaceType"/>.
/// </remarks>
public RendererType ResolvedRenderer { get; private set; }
/// <summary>
/// All valid <see cref="RendererType"/>s for the current platform, in order of how stable and performant they are deemed to be.
/// </summary>
public IEnumerable<RendererType> GetPreferredRenderersForCurrentPlatform()
{
yield return RendererType.Automatic;
// Preferred per-platform renderers
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
yield return RendererType.Direct3D11;
yield return RendererType.Deferred_Direct3D11;
yield return RendererType.OpenGL;
yield return RendererType.Deferred_Vulkan;
break;
case RuntimeInfo.Platform.Linux:
yield return RendererType.OpenGL;
yield return RendererType.Deferred_OpenGL;
yield return RendererType.Deferred_Vulkan;
break;
case RuntimeInfo.Platform.macOS:
yield return RendererType.Metal;
yield return RendererType.Deferred_Metal;
yield return RendererType.OpenGL;
break;
case RuntimeInfo.Platform.iOS:
// GL renderer not supported, see: https://github.com/ppy/osu/issues/23003.
yield return RendererType.Metal;
yield return RendererType.Deferred_Metal;
break;
case RuntimeInfo.Platform.Android:
// Still uses osuTK so only the legacy GL renderer is supported.
yield return RendererType.OpenGL;
break;
}
}
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<RendererType>(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
{
switch (type)
{
case RendererType.OpenGL:
// Use the legacy GL renderer. This is basically guaranteed to support all platforms
// and performs better than the Veldrid-GL renderer due to reduction in allocs.
SetupRendererAndWindow(new GLRenderer(), GraphicsSurfaceType.OpenGL);
break;
case RendererType.Deferred_Metal:
case RendererType.Deferred_Vulkan:
case RendererType.Deferred_Direct3D11:
case RendererType.Deferred_OpenGL:
SetupRendererAndWindow(new DeferredRenderer(), rendererToGraphicsSurfaceType(type));
break;
default:
SetupRendererAndWindow(new VeldridRenderer(), rendererToGraphicsSurfaceType(type));
break;
}
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.Deferred_Metal:
case RendererType.Metal:
surface = GraphicsSurfaceType.Metal;
break;
case RendererType.Deferred_Vulkan:
case RendererType.Vulkan:
surface = GraphicsSurfaceType.Vulkan;
break;
case RendererType.Deferred_Direct3D11:
case RendererType.Direct3D11:
surface = GraphicsSurfaceType.Direct3D11;
break;
case RendererType.Deferred_OpenGL:
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;
case "deferred":
SetupRendererAndWindow(new DeferredRenderer(), 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;
Renderer.CacheStorage = CacheStorage.GetStorageForDirectory("shaders");
try
{
// Prepare window
Window = CreateWindow(surfaceType);
if (Window == null)
{
// Can be null, usually via Headless execution.
if (!RequireWindowExists)
return;
throw new InvalidOperationException("🖼️ Renderer could not be initialised as window creation failed.");
}
Window.SetupWindow(Config);
Window.Create();
Window.Title = Options.FriendlyGameName;
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());
Window.CurrentDisplayBindable.BindValueChanged(display =>
{
if (Renderer is VeldridRenderer veldridRenderer)
{
Rectangle bounds = display.NewValue.Bounds;
veldridRenderer.Device.UpdateActiveDisplay(bounds.X, bounds.Y, bounds.Width, bounds.Height);
}
}, true);
IsActive.BindTo(Window.IsActive);
AllowScreenSuspension.Result.BindValueChanged(e =>
{
if (e.NewValue)
Window.EnableScreenSuspension();
else
Window.DisableScreenSuspension();
}, true);
}
/// <summary>
/// Finds the default <see cref="Storage"/> for the game to be used if <see cref="Game.CreateStorage"/> is not overridden.
/// </summary>
/// <returns>The <see cref="Storage"/>.</returns>
protected virtual Storage GetDefaultGameStorage()
{
// first check all valid paths for any existing install.
foreach (string path in UserStoragePaths)
{
var storage = GetStorage(path);
// if an existing data directory exists for this application, prefer it immediately.
if (storage.ExistsDirectory(Name))
return storage.GetStorageForDirectory(Name);
}
// if an existing directory could not be found, use the first path that can be created.
foreach (string path in UserStoragePaths)
{
try
{
return GetStorage(path).GetStorageForDirectory(Name);
}
catch
{
// may fail on directory creation.
}
}
throw new InvalidOperationException("No valid user storage path could be resolved.");
}
/// <summary>
/// Pauses all active threads. Call <see cref="Resume"/> to resume execution.
/// </summary>
public void Suspend()
{
suspended = true;
threadRunner.Suspend();
}
/// <summary>
/// Resumes all of the current paused threads after <see cref="Suspend"/> was called.
/// </summary>
public void Resume()
{
threadRunner.Start();
suspended = false;
}
private ThreadRunner threadRunner;
private void windowUpdate()
{
inputPerformanceCollectionPeriod?.Dispose();
inputPerformanceCollectionPeriod = null;
if (suspended)
return;
threadRunner.RunMainLoop();
inputPerformanceCollectionPeriod = inputMonitor.BeginCollecting(PerformanceCollectionType.WndProc);
}
/// <summary>
/// Prepare this game host for <see cref="Run"/>.
/// <remarks>
/// <see cref="Storage"/> is available here.
/// </remarks>
/// </summary>
protected virtual void SetupForRun()
{
Logger.Storage = Storage.GetStorageForDirectory("logs");
Logger.Enabled = true;
}
private void populateInputHandlers()
{
AvailableInputHandlers = CreateAvailableInputHandlers().ToImmutableArray();
}
private void initialiseInputHandlers()
{
foreach (var handler in AvailableInputHandlers)
{
if (!handler.Initialize(this))
handler.Enabled.Value = false;
}
}
/// <summary>
/// Reset all input handlers' settings to a default state.
/// </summary>
public void ResetInputHandlers()
{
// restore any disable handlers per legacy configuration.
ignoredInputHandlers.TriggerChange();
foreach (var handler in AvailableInputHandlers)
{
handler.Reset();
}
}
/// <summary>
/// The clock which is to be used by the scene graph (will be assigned to <see cref="Root"/>).
/// </summary>
protected virtual IFrameBasedClock SceneGraphClock => UpdateThread.Clock;
private void bootstrapSceneGraph(Game game)
{
var root = game.CreateUserInputManager();
root.Child = new PlatformActionContainer
{
Child = new FrameworkActionContainer
{
Child = new SafeAreaDefiningContainer
{
RelativeSizeAxes = Axes.Both,
Child = game
}
}
};
Dependencies.Cache(root);
Dependencies.CacheAs(game);
game.SetHost(this);
root.Load(SceneGraphClock, Dependencies);
//publish bootstrapped scene graph to all threads.
Root = root;
}
private InvokeOnDisposal inputPerformanceCollectionPeriod;
private Bindable<bool> bypassFrontToBackPass;
private Bindable<FrameSync> frameSyncMode;
private IBindable<DisplayMode> currentDisplayMode;
private Bindable<string> ignoredInputHandlers;
private readonly Bindable<double> cursorSensitivity = new Bindable<double>(1);
public readonly Bindable<bool> PerformanceLogging = new Bindable<bool>();
private Bindable<WindowMode> windowMode;
private Bindable<ExecutionMode> executionMode;
private Bindable<string> threadLocale;
protected virtual void SetupConfig(IDictionary<FrameworkSetting, object> defaultOverrides)
{
if (!defaultOverrides.ContainsKey(FrameworkSetting.WindowMode))
defaultOverrides.Add(FrameworkSetting.WindowMode, Window?.DefaultWindowMode ?? WindowMode.Windowed);
Dependencies.Cache(DebugConfig = new FrameworkDebugConfigManager());
Dependencies.Cache(Config = new FrameworkConfigManager(Storage, defaultOverrides));
windowMode = Config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
windowMode.BindValueChanged(mode =>
{
if (Window == null)
return;
if (!Window.SupportedWindowModes.Contains(mode.NewValue))
windowMode.Value = Window.DefaultWindowMode;
}, true);
executionMode = Config.GetBindable<ExecutionMode>(FrameworkSetting.ExecutionMode);
executionMode.BindValueChanged(e => threadRunner.ExecutionMode = e.NewValue, true);
frameSyncMode = Config.GetBindable<FrameSync>(FrameworkSetting.FrameSync);
frameSyncMode.ValueChanged += _ => updateFrameSyncMode();
#pragma warning disable 618
// pragma region can be removed 20210911
ignoredInputHandlers = Config.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers);
ignoredInputHandlers.ValueChanged += e =>
{
var configIgnores = e.NewValue.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s));
foreach (var handler in AvailableInputHandlers)
{
string handlerType = handler.ToString();
handler.Enabled.Value = configIgnores.All(ch => ch != handlerType);
}
};
Config.BindWith(FrameworkSetting.CursorSensitivity, cursorSensitivity);
var cursorSensitivityHandlers = AvailableInputHandlers.OfType<IHasCursorSensitivity>();
// one way bindings to preserve compatibility.
cursorSensitivity.BindValueChanged(val =>
{
foreach (var h in cursorSensitivityHandlers)
h.Sensitivity.Value = val.NewValue;
}, true);
foreach (var h in cursorSensitivityHandlers)
h.Sensitivity.BindValueChanged(s => cursorSensitivity.Value = s.NewValue);
#pragma warning restore 618
PerformanceLogging.BindValueChanged(logging =>
{
Threads.ForEach(t =>
{
if (t.Monitor != null)
t.Monitor.EnablePerformanceProfiling = logging.NewValue;
});
DebugUtils.LogPerformanceIssues = logging.NewValue;
TypePerformanceMonitor.Active = logging.NewValue;
}, true);
bypassFrontToBackPass = DebugConfig.GetBindable<bool>(DebugSetting.BypassFrontToBackPass);
threadLocale = Config.GetBindable<string>(FrameworkSetting.Locale);
threadLocale.BindValueChanged(locale =>
{
// return value of TryGet ignored as the failure case gives expected results (CultureInfo.InvariantCulture)
CultureInfoHelper.TryGetCultureInfo(locale.NewValue, out var culture);
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
threadRunner.SetCulture(culture);
}, true);
inputConfig = new InputConfigManager(Storage, AvailableInputHandlers);
#pragma warning disable CS0612 // Type or member is obsolete
if (Config.Get<RendererType>(FrameworkSetting.Renderer) == RendererType.OpenGLLegacy)
Config.SetValue(FrameworkSetting.Renderer, RendererType.OpenGL);
#pragma warning restore CS0612 // Type or member is obsolete
}
/// <summary>
/// Games using osu!framework can generally run at *very* high frame rates when not much is going on.
///
/// This can be counter-productive due to the induced allocation and GPU overhead.
/// - Allocation overhead can lead to excess garbage collection
/// - GPU overhead can lead to unexpected pipeline blocking (and stutters as a result).
/// Also, in general graphics card manufacturers do not test their hardware at insane frame rates and
/// therefore drivers are not optimised to handle this kind of throughput.
/// - We only harvest input at 1000hz, so running any higher has zero benefits.
///
/// We limit things to the same rate we poll input at, to keep both gamers and their systems happy
/// and (more) stutter-free.
/// </summary>
private const int maximum_sane_fps = GameThread.DEFAULT_ACTIVE_HZ;
private void updateFrameSyncMode()
{
if (Window == null)
return;
int refreshRate = (int)MathF.Round(Window.CurrentDisplayMode.Value.RefreshRate);
// For invalid refresh rates let's assume 60 Hz as it is most common.
if (refreshRate <= 0)
refreshRate = 60;
int drawLimiter = refreshRate;
int updateLimiter = drawLimiter * 2;
setVSyncMode();
switch (frameSyncMode.Value)
{
case FrameSync.VSync:
drawLimiter = int.MaxValue;
updateLimiter *= 2;
break;
case FrameSync.Limit2x:
drawLimiter *= 2;
updateLimiter *= 2;
break;
case FrameSync.Limit4x:
drawLimiter *= 4;
updateLimiter *= 4;
break;
case FrameSync.Limit8x:
drawLimiter *= 8;
updateLimiter *= 8;
break;
case FrameSync.Unlimited:
drawLimiter = int.MaxValue;
updateLimiter = int.MaxValue;
break;
}
if (!AllowBenchmarkUnlimitedFrames)
{
drawLimiter = Math.Min(maximum_sane_fps, drawLimiter);
updateLimiter = Math.Min(maximum_sane_fps, updateLimiter);
}
MaximumDrawHz = drawLimiter;
MaximumUpdateHz = updateLimiter;
}
private void setVSyncMode()
{
if (Window == null) return;
DrawThread.Scheduler.Add(() => Renderer.VerticalSync = frameSyncMode.Value == FrameSync.VSync);
}
/// <summary>
/// 请求更改渲染器的 VSync 状态。
/// </summary>
/// <remarks>
/// 底层渲染器属性为 <c>protected internal</c>,只能在 osu!framework 程序集中修改。
/// 提供该方法是为了让宿主游戏可以应用“仅影响 draw 的策略”(例如:在非 gameplay 界面禁用 VSync
/// 同时不影响 update 频率。
/// </remarks>
public void SetVerticalSync(bool enabled)
{
if (Window == null)
return;
DrawThread?.Scheduler.Add(() => Renderer.VerticalSync = enabled);
}
/// <summary>
/// 设置每帧允许上传到 GPU 的纹理数量与像素预算上限。
/// </summary>
/// <remarks>
/// 宿主游戏可用此在 UI 较重的界面中,用“更慢的纹理流式加载”换取“更平滑的帧时间”。
/// 设置会在 draw 线程上生效。
/// </remarks>
/// <param name="maxTexturesUploadedPerFrame">每帧最多上传的纹理数量。必须 &gt; 0。</param>
/// <param name="maxPixelsUploadedPerFrame">每帧最多上传的像素数量。必须 &gt; 0。</param>
/// <param name="rememberOriginal">
/// 是否记录原始限制(仅首次调用生效),以便后续可通过 <see cref="RestoreTextureUploadLimits"/> 还原。
/// </param>
public void SetTextureUploadLimits(int maxTexturesUploadedPerFrame, int maxPixelsUploadedPerFrame, bool rememberOriginal = true)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxTexturesUploadedPerFrame);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxPixelsUploadedPerFrame);
if (Renderer is not Renderer renderer)
return;
DrawThread?.Scheduler.Add(() =>
{
if (rememberOriginal)
{
originalMaxTexturesUploadedPerFrame ??= renderer.MaxTexturesUploadedPerFrame;
originalMaxPixelsUploadedPerFrame ??= renderer.MaxPixelsUploadedPerFrame;
}
renderer.MaxTexturesUploadedPerFrame = maxTexturesUploadedPerFrame;
renderer.MaxPixelsUploadedPerFrame = maxPixelsUploadedPerFrame;
});
}
/// <summary>
/// 还原先前由 <see cref="SetTextureUploadLimits"/> 捕获的纹理上传限制。
/// </summary>
public void RestoreTextureUploadLimits()
{
if (Renderer is not Renderer renderer)
return;
if (originalMaxTexturesUploadedPerFrame == null || originalMaxPixelsUploadedPerFrame == null)
return;
DrawThread?.Scheduler.Add(() =>
{
renderer.MaxTexturesUploadedPerFrame = originalMaxTexturesUploadedPerFrame.Value;
renderer.MaxPixelsUploadedPerFrame = originalMaxPixelsUploadedPerFrame.Value;
});
}
/// <summary>
/// Construct all input handlers for this host. The order here decides the priority given to handlers, with the earliest occurring having higher priority.
/// </summary>
protected abstract IEnumerable<InputHandler> CreateAvailableInputHandlers();
public ImmutableArray<InputHandler> AvailableInputHandlers { get; private set; }
protected virtual TextInputSource CreateTextInput() => new TextInputSource();
#region IDisposable Support
private bool isDisposed;
private readonly ManualResetEventSlim stoppedEvent = new ManualResetEventSlim(false);
protected virtual void Dispose(bool disposing)
{
if (isDisposed)
return;
isDisposed = true;
switch (ExecutionState)
{
case ExecutionState.Running:
throw new InvalidOperationException($"{nameof(Exit)} must be called before the {nameof(GameHost)} is disposed.");
case ExecutionState.Stopping:
case ExecutionState.Stopped:
// Delay disposal until the game has exited
if (!stoppedEvent.Wait(60000))
throw new InvalidOperationException("Game stuck in runnning state.");
break;
}
AppDomain.CurrentDomain.UnhandledException -= unhandledExceptionHandler;
TaskScheduler.UnobservedTaskException -= unobservedExceptionHandler;
Root?.Dispose();
Root = null;
stoppedEvent.Dispose();
inputConfig?.Dispose();
Config?.Dispose();
DebugConfig?.Dispose();
Window?.Dispose();
LoadingComponentsLogger.LogAndFlush();
Logger.FlushForShutdown();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
/// <summary>
/// Defines the platform-specific key bindings that will be used by <see cref="PlatformActionContainer"/>.
/// Should be overridden per-platform to provide native key bindings.
/// </summary>
public virtual IEnumerable<KeyBinding> PlatformKeyBindings => new[]
{
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.X), PlatformAction.Cut),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.C), PlatformAction.Copy),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.V), PlatformAction.Paste),
new KeyBinding(new KeyCombination(InputKey.Shift, InputKey.Delete), PlatformAction.Cut),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Insert), PlatformAction.Copy),
new KeyBinding(new KeyCombination(InputKey.Shift, InputKey.Insert), PlatformAction.Paste),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.A), PlatformAction.SelectAll),
new KeyBinding(InputKey.Left, PlatformAction.MoveBackwardChar),
new KeyBinding(InputKey.Right, PlatformAction.MoveForwardChar),
new KeyBinding(InputKey.BackSpace, PlatformAction.DeleteBackwardChar),
new KeyBinding(InputKey.Delete, PlatformAction.DeleteForwardChar),
new KeyBinding(new KeyCombination(InputKey.Shift, InputKey.Left), PlatformAction.SelectBackwardChar),
new KeyBinding(new KeyCombination(InputKey.Shift, InputKey.Right), PlatformAction.SelectForwardChar),
new KeyBinding(new KeyCombination(InputKey.Shift, InputKey.BackSpace), PlatformAction.DeleteBackwardChar),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Left), PlatformAction.MoveBackwardWord),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Right), PlatformAction.MoveForwardWord),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.BackSpace), PlatformAction.DeleteBackwardWord),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Delete), PlatformAction.DeleteForwardWord),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.Left), PlatformAction.SelectBackwardWord),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.Right), PlatformAction.SelectForwardWord),
new KeyBinding(InputKey.Home, PlatformAction.MoveBackwardLine),
new KeyBinding(InputKey.End, PlatformAction.MoveForwardLine),
new KeyBinding(new KeyCombination(InputKey.Shift, InputKey.Home), PlatformAction.SelectBackwardLine),
new KeyBinding(new KeyCombination(InputKey.Shift, InputKey.End), PlatformAction.SelectForwardLine),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.PageUp), PlatformAction.DocumentPrevious),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.PageDown), PlatformAction.DocumentNext),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Tab), PlatformAction.DocumentNext),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.Tab), PlatformAction.DocumentPrevious),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.W), PlatformAction.DocumentClose),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.F4), PlatformAction.DocumentClose),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.N), PlatformAction.DocumentNew),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.T), PlatformAction.TabNew),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.T), PlatformAction.TabRestore),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.S), PlatformAction.Save),
new KeyBinding(InputKey.Home, PlatformAction.MoveToListStart),
new KeyBinding(InputKey.End, PlatformAction.MoveToListEnd),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Z), PlatformAction.Undo),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Y), PlatformAction.Redo),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.Z), PlatformAction.Redo),
new KeyBinding(InputKey.Delete, PlatformAction.Delete),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Plus), PlatformAction.ZoomIn),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.KeypadPlus), PlatformAction.ZoomIn),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Minus), PlatformAction.ZoomOut),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.KeypadMinus), PlatformAction.ZoomOut),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Number0), PlatformAction.ZoomDefault),
new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Keypad0), PlatformAction.ZoomDefault),
};
/// <summary>
/// Create a texture loader store based on an underlying data store.
/// </summary>
/// <param name="underlyingStore">The underlying provider of texture data (in arbitrary image formats).</param>
/// <returns>A texture loader store.</returns>
public virtual IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore)
=> new TextureLoaderStore(underlyingStore);
/// <summary>
/// Create a <see cref="VideoDecoder"/> with the given stream. May be overridden by platforms that require a different
/// decoder implementation.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to decode.</param>
/// <returns>An instance of <see cref="VideoDecoder"/> initialised with the given stream.</returns>
public virtual VideoDecoder CreateVideoDecoder(Stream stream) => new VideoDecoder(Renderer, stream);
/// <summary>
/// Creates the <see cref="ThreadRunner"/> to run the threads of this <see cref="GameHost"/>.
/// </summary>
/// <param name="mainThread">The main thread.</param>
/// <returns>The <see cref="ThreadRunner"/>.</returns>
protected virtual ThreadRunner CreateThreadRunner(InputThread mainThread) => new ThreadRunner(mainThread);
}
/// <summary>
/// The game's execution states. All of these states can only be present once per <see cref="GameHost"/>.
/// Note: The order of values in this enum matters.
/// </summary>
public enum ExecutionState
{
/// <summary>
/// <see cref="GameHost.Run"/> has not been invoked yet.
/// </summary>
Idle = 0,
/// <summary>
/// The game's execution has completely stopped.
/// </summary>
Stopped = 1,
/// <summary>
/// The user has invoked <see cref="GameHost.Exit"/>, or the window has been called.
/// The game is currently awaiting to stop all execution on the correct thread.
/// </summary>
Stopping = 2,
/// <summary>
/// <see cref="GameHost.Run"/> has been invoked.
/// </summary>
Running = 3
}
}