aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs')
-rw-r--r--src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs803
1 files changed, 803 insertions, 0 deletions
diff --git a/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs b/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs
new file mode 100644
index 00000000..e27d0604
--- /dev/null
+++ b/src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs
@@ -0,0 +1,803 @@
+using Gdk;
+using Gtk;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.GAL.Multithreading;
+using Ryujinx.Graphics.Gpu;
+using Ryujinx.Input;
+using Ryujinx.Input.GTK3;
+using Ryujinx.Input.HLE;
+using Ryujinx.UI.Common.Configuration;
+using Ryujinx.UI.Common.Helper;
+using Ryujinx.UI.Widgets;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Image = SixLabors.ImageSharp.Image;
+using Key = Ryujinx.Input.Key;
+using ScalingFilter = Ryujinx.Graphics.GAL.ScalingFilter;
+using Switch = Ryujinx.HLE.Switch;
+
+namespace Ryujinx.UI
+{
+ public abstract class RendererWidgetBase : DrawingArea
+ {
+ private const int SwitchPanelWidth = 1280;
+ private const int SwitchPanelHeight = 720;
+ private const int TargetFps = 60;
+ private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
+ private const float VolumeDelta = 0.05f;
+
+ public ManualResetEvent WaitEvent { get; set; }
+ public NpadManager NpadManager { get; }
+ public TouchScreenManager TouchScreenManager { get; }
+ public Switch Device { get; private set; }
+ public IRenderer Renderer { get; private set; }
+
+ public bool ScreenshotRequested { get; set; }
+ protected int WindowWidth { get; private set; }
+ protected int WindowHeight { get; private set; }
+
+ public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
+
+ private bool _isActive;
+ private bool _isStopped;
+
+ private bool _toggleFullscreen;
+ private bool _toggleDockedMode;
+
+ private readonly long _ticksPerFrame;
+
+ private long _ticks = 0;
+ private float _newVolume;
+
+ private readonly Stopwatch _chrono;
+
+ private KeyboardHotkeyState _prevHotkeyState;
+
+ private readonly ManualResetEvent _exitEvent;
+ private readonly ManualResetEvent _gpuDoneEvent;
+
+ private readonly CancellationTokenSource _gpuCancellationTokenSource;
+
+ // Hide Cursor
+ const int CursorHideIdleTime = 5; // seconds
+ private static readonly Cursor _invisibleCursor = new(Display.Default, CursorType.BlankCursor);
+ private long _lastCursorMoveTime;
+ private HideCursorMode _hideCursorMode;
+ private readonly InputManager _inputManager;
+ private readonly IKeyboard _keyboardInterface;
+ private readonly GraphicsDebugLevel _glLogLevel;
+ private string _gpuBackendName;
+ private string _gpuDriverName;
+ private bool _isMouseInClient;
+
+ public RendererWidgetBase(InputManager inputManager, GraphicsDebugLevel glLogLevel)
+ {
+ var mouseDriver = new GTK3MouseDriver(this);
+
+ _inputManager = inputManager;
+ _inputManager.SetMouseDriver(mouseDriver);
+ NpadManager = _inputManager.CreateNpadManager();
+ TouchScreenManager = _inputManager.CreateTouchScreenManager();
+ _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
+
+ WaitEvent = new ManualResetEvent(false);
+
+ _glLogLevel = glLogLevel;
+
+ Destroyed += Renderer_Destroyed;
+
+ _chrono = new Stopwatch();
+
+ _ticksPerFrame = Stopwatch.Frequency / TargetFps;
+
+ AddEvents((int)(EventMask.ButtonPressMask
+ | EventMask.ButtonReleaseMask
+ | EventMask.PointerMotionMask
+ | EventMask.ScrollMask
+ | EventMask.EnterNotifyMask
+ | EventMask.LeaveNotifyMask
+ | EventMask.KeyPressMask
+ | EventMask.KeyReleaseMask));
+
+ _exitEvent = new ManualResetEvent(false);
+ _gpuDoneEvent = new ManualResetEvent(false);
+
+ _gpuCancellationTokenSource = new CancellationTokenSource();
+
+ _hideCursorMode = ConfigurationState.Instance.HideCursor;
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+
+ ConfigurationState.Instance.HideCursor.Event += HideCursorStateChanged;
+ ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAnriAliasing;
+ ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
+ ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
+ }
+
+ private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs<int> e)
+ {
+ Renderer.Window.SetScalingFilter((ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
+ Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
+ }
+
+ private void UpdateScalingFilter(object sender, ReactiveEventArgs<Ryujinx.Common.Configuration.ScalingFilter> e)
+ {
+ Renderer.Window.SetScalingFilter((ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
+ Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
+ }
+
+ public abstract void InitializeRenderer();
+
+ public abstract void SwapBuffers();
+
+ protected abstract string GetGpuBackendName();
+
+ private string GetGpuDriverName()
+ {
+ return Renderer.GetHardwareInfo().GpuDriver;
+ }
+
+ private void HideCursorStateChanged(object sender, ReactiveEventArgs<HideCursorMode> state)
+ {
+ Application.Invoke(delegate
+ {
+ _hideCursorMode = state.NewValue;
+
+ switch (_hideCursorMode)
+ {
+ case HideCursorMode.Never:
+ Window.Cursor = null;
+ break;
+ case HideCursorMode.OnIdle:
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+ break;
+ case HideCursorMode.Always:
+ Window.Cursor = _invisibleCursor;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(state));
+ }
+ });
+ }
+
+ private void Renderer_Destroyed(object sender, EventArgs e)
+ {
+ ConfigurationState.Instance.HideCursor.Event -= HideCursorStateChanged;
+ ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAnriAliasing;
+ ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter;
+ ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel;
+
+ NpadManager.Dispose();
+ Dispose();
+ }
+
+ private void UpdateAnriAliasing(object sender, ReactiveEventArgs<Ryujinx.Common.Configuration.AntiAliasing> e)
+ {
+ Renderer?.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)e.NewValue);
+ }
+
+ protected override bool OnMotionNotifyEvent(EventMotion evnt)
+ {
+ if (_hideCursorMode == HideCursorMode.OnIdle)
+ {
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+ }
+
+ if (ConfigurationState.Instance.Hid.EnableMouse)
+ {
+ Window.Cursor = _invisibleCursor;
+ }
+
+ _isMouseInClient = true;
+
+ return false;
+ }
+
+ protected override bool OnEnterNotifyEvent(EventCrossing evnt)
+ {
+ Window.Cursor = ConfigurationState.Instance.Hid.EnableMouse ? _invisibleCursor : null;
+
+ _isMouseInClient = true;
+
+ return base.OnEnterNotifyEvent(evnt);
+ }
+
+ protected override bool OnLeaveNotifyEvent(EventCrossing evnt)
+ {
+ Window.Cursor = null;
+
+ _isMouseInClient = false;
+
+ return base.OnLeaveNotifyEvent(evnt);
+ }
+
+ protected override void OnGetPreferredHeight(out int minimumHeight, out int naturalHeight)
+ {
+ Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
+
+ // If the monitor is at least 1080p, use the Switch panel size as minimal size.
+ if (monitor.Geometry.Height >= 1080)
+ {
+ minimumHeight = SwitchPanelHeight;
+ }
+ // Otherwise, we default minimal size to 480p 16:9.
+ else
+ {
+ minimumHeight = 480;
+ }
+
+ naturalHeight = minimumHeight;
+ }
+
+ protected override void OnGetPreferredWidth(out int minimumWidth, out int naturalWidth)
+ {
+ Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
+
+ // If the monitor is at least 1080p, use the Switch panel size as minimal size.
+ if (monitor.Geometry.Height >= 1080)
+ {
+ minimumWidth = SwitchPanelWidth;
+ }
+ // Otherwise, we default minimal size to 480p 16:9.
+ else
+ {
+ minimumWidth = 854;
+ }
+
+ naturalWidth = minimumWidth;
+ }
+
+ protected override bool OnConfigureEvent(EventConfigure evnt)
+ {
+ bool result = base.OnConfigureEvent(evnt);
+
+ Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
+
+ WindowWidth = evnt.Width * monitor.ScaleFactor;
+ WindowHeight = evnt.Height * monitor.ScaleFactor;
+
+ Renderer?.Window?.SetSize(WindowWidth, WindowHeight);
+
+ return result;
+ }
+
+ private void HandleScreenState(KeyboardStateSnapshot keyboard)
+ {
+ bool toggleFullscreen = keyboard.IsPressed(Key.F11)
+ || ((keyboard.IsPressed(Key.AltLeft)
+ || keyboard.IsPressed(Key.AltRight))
+ && keyboard.IsPressed(Key.Enter))
+ || keyboard.IsPressed(Key.Escape);
+
+ bool fullScreenToggled = ParentWindow.State.HasFlag(WindowState.Fullscreen);
+
+ if (toggleFullscreen != _toggleFullscreen)
+ {
+ if (toggleFullscreen)
+ {
+ if (fullScreenToggled)
+ {
+ ParentWindow.Unfullscreen();
+ (Toplevel as MainWindow)?.ToggleExtraWidgets(true);
+ }
+ else
+ {
+ if (keyboard.IsPressed(Key.Escape))
+ {
+ if (!ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog())
+ {
+ Exit();
+ }
+ }
+ else
+ {
+ ParentWindow.Fullscreen();
+ (Toplevel as MainWindow)?.ToggleExtraWidgets(false);
+ }
+ }
+ }
+ }
+
+ _toggleFullscreen = toggleFullscreen;
+
+ bool toggleDockedMode = keyboard.IsPressed(Key.F9);
+
+ if (toggleDockedMode != _toggleDockedMode)
+ {
+ if (toggleDockedMode)
+ {
+ ConfigurationState.Instance.System.EnableDockedMode.Value =
+ !ConfigurationState.Instance.System.EnableDockedMode.Value;
+ }
+ }
+
+ _toggleDockedMode = toggleDockedMode;
+
+ if (_isMouseInClient)
+ {
+ if (ConfigurationState.Instance.Hid.EnableMouse.Value)
+ {
+ Window.Cursor = _invisibleCursor;
+ }
+ else
+ {
+ switch (_hideCursorMode)
+ {
+ case HideCursorMode.OnIdle:
+ long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
+ Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null;
+ break;
+ case HideCursorMode.Always:
+ Window.Cursor = _invisibleCursor;
+ break;
+ case HideCursorMode.Never:
+ Window.Cursor = null;
+ break;
+ }
+ }
+ }
+ }
+
+ public void Initialize(Switch device)
+ {
+ Device = device;
+
+ IRenderer renderer = Device.Gpu.Renderer;
+
+ if (renderer is ThreadedRenderer tr)
+ {
+ renderer = tr.BaseRenderer;
+ }
+
+ Renderer = renderer;
+ Renderer?.Window?.SetSize(WindowWidth, WindowHeight);
+
+ if (Renderer != null)
+ {
+ Renderer.ScreenCaptured += Renderer_ScreenCaptured;
+ }
+
+ NpadManager.Initialize(device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
+ TouchScreenManager.Initialize(device);
+ }
+
+ private unsafe void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e)
+ {
+ if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0)
+ {
+ Task.Run(() =>
+ {
+ lock (this)
+ {
+ var currentTime = DateTime.Now;
+ string filename = $"ryujinx_capture_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png";
+ string directory = AppDataManager.Mode switch
+ {
+ AppDataManager.LaunchMode.Portable or AppDataManager.LaunchMode.Custom => System.IO.Path.Combine(AppDataManager.BaseDirPath, "screenshots"),
+ _ => System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx"),
+ };
+
+ string path = System.IO.Path.Combine(directory, filename);
+
+ try
+ {
+ Directory.CreateDirectory(directory);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {directory}. Error : {ex.GetType().Name}", "Screenshot");
+
+ return;
+ }
+
+ Image image = e.IsBgra ? Image.LoadPixelData<Bgra32>(e.Data, e.Width, e.Height)
+ : Image.LoadPixelData<Rgba32>(e.Data, e.Width, e.Height);
+
+ if (e.FlipX)
+ {
+ image.Mutate(x => x.Flip(FlipMode.Horizontal));
+ }
+
+ if (e.FlipY)
+ {
+ image.Mutate(x => x.Flip(FlipMode.Vertical));
+ }
+
+ image.SaveAsPng(path, new PngEncoder()
+ {
+ ColorType = PngColorType.Rgb,
+ });
+
+ image.Dispose();
+
+ Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
+ }
+ });
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Screenshot is empty. Size : {e.Data.Length} bytes. Resolution : {e.Width}x{e.Height}", "Screenshot");
+ }
+ }
+
+ public void Render()
+ {
+ Gtk.Window parent = Toplevel as Gtk.Window;
+ parent.Present();
+
+ InitializeRenderer();
+
+ Device.Gpu.Renderer.Initialize(_glLogLevel);
+
+ Renderer.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)ConfigurationState.Instance.Graphics.AntiAliasing.Value);
+ Renderer.Window.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
+ Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
+
+ _gpuBackendName = GetGpuBackendName();
+ _gpuDriverName = GetGpuDriverName();
+
+ Device.Gpu.Renderer.RunLoop(() =>
+ {
+ Device.Gpu.SetGpuThread();
+ Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
+
+ Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
+
+ (Toplevel as MainWindow)?.ActivatePauseMenu();
+
+ while (_isActive)
+ {
+ if (_isStopped)
+ {
+ return;
+ }
+
+ _ticks += _chrono.ElapsedTicks;
+
+ _chrono.Restart();
+
+ if (Device.WaitFifo())
+ {
+ Device.Statistics.RecordFifoStart();
+ Device.ProcessFrame();
+ Device.Statistics.RecordFifoEnd();
+ }
+
+ while (Device.ConsumeFrameAvailable())
+ {
+ Device.PresentFrame(SwapBuffers);
+ }
+
+ if (_ticks >= _ticksPerFrame)
+ {
+ string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? "Docked" : "Handheld";
+ float scale = GraphicsConfig.ResScale;
+ if (scale != 1)
+ {
+ dockedMode += $" ({scale}x)";
+ }
+
+ StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
+ Device.EnableDeviceVsync,
+ Device.GetVolume(),
+ _gpuBackendName,
+ dockedMode,
+ ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
+ $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
+ $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
+ $"GPU: {_gpuDriverName}"));
+
+ _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
+ }
+ }
+
+ // Make sure all commands in the run loop are fully executed before leaving the loop.
+ if (Device.Gpu.Renderer is ThreadedRenderer threaded)
+ {
+ threaded.FlushThreadedCommands();
+ }
+
+ _gpuDoneEvent.Set();
+ });
+ }
+
+ public void Start()
+ {
+ _chrono.Restart();
+
+ _isActive = true;
+
+ Gtk.Window parent = Toplevel as Gtk.Window;
+
+ Application.Invoke(delegate
+ {
+ parent.Present();
+
+ var activeProcess = Device.Processes.ActiveApplication;
+
+ parent.Title = TitleHelper.ActiveApplicationTitle(activeProcess, Program.Version);
+ });
+
+ Thread renderLoopThread = new(Render)
+ {
+ Name = "GUI.RenderLoop",
+ };
+ renderLoopThread.Start();
+
+ Thread nvidiaStutterWorkaround = null;
+ if (Renderer is Graphics.OpenGL.OpenGLRenderer)
+ {
+ nvidiaStutterWorkaround = new Thread(NvidiaStutterWorkaround)
+ {
+ Name = "GUI.NvidiaStutterWorkaround",
+ };
+ nvidiaStutterWorkaround.Start();
+ }
+
+ MainLoop();
+
+ // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
+ // We only need to wait for all commands submitted during the main gpu loop to be processed.
+ _gpuDoneEvent.WaitOne();
+ _gpuDoneEvent.Dispose();
+ nvidiaStutterWorkaround?.Join();
+
+ Exit();
+ }
+
+ public void Exit()
+ {
+ TouchScreenManager?.Dispose();
+ NpadManager?.Dispose();
+
+ if (_isStopped)
+ {
+ return;
+ }
+
+ _gpuCancellationTokenSource.Cancel();
+
+ _isStopped = true;
+
+ if (_isActive)
+ {
+ _isActive = false;
+
+ _exitEvent.WaitOne();
+ _exitEvent.Dispose();
+ }
+ }
+
+ private void NvidiaStutterWorkaround()
+ {
+ while (_isActive)
+ {
+ // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones.
+ // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity.
+ // However, it immediately starts up again, since the rules regarding when to terminate and when to start differ.
+ // This creates a new thread every second or so.
+ // The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics.
+ // This is a little over budget on a frame time of 16ms, so creates a large stutter.
+ // The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread.
+
+ // TODO: This should be removed when the issue with the GateThread is resolved.
+
+ ThreadPool.QueueUserWorkItem((state) => { });
+ Thread.Sleep(300);
+ }
+ }
+
+ public void MainLoop()
+ {
+ while (_isActive)
+ {
+ UpdateFrame();
+
+ // Polling becomes expensive if it's not slept
+ Thread.Sleep(1);
+ }
+
+ _exitEvent.Set();
+ }
+
+ private bool UpdateFrame()
+ {
+ if (!_isActive)
+ {
+ return true;
+ }
+
+ if (_isStopped)
+ {
+ return false;
+ }
+
+ if ((Toplevel as MainWindow).IsFocused)
+ {
+ Application.Invoke(delegate
+ {
+ KeyboardStateSnapshot keyboard = _keyboardInterface.GetKeyboardStateSnapshot();
+
+ HandleScreenState(keyboard);
+
+ if (keyboard.IsPressed(Key.Delete))
+ {
+ if (!ParentWindow.State.HasFlag(WindowState.Fullscreen))
+ {
+ Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel();
+ }
+ }
+ });
+ }
+
+ NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
+
+ if ((Toplevel as MainWindow).IsFocused)
+ {
+ KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync))
+ {
+ Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
+ }
+
+ if ((currentHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot)) || ScreenshotRequested)
+ {
+ ScreenshotRequested = false;
+
+ Renderer.Screenshot();
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ShowUI) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ShowUI))
+ {
+ (Toplevel as MainWindow).ToggleExtraWidgets(true);
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.Pause) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Pause))
+ {
+ (Toplevel as MainWindow)?.TogglePause();
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute))
+ {
+ if (Device.IsAudioMuted())
+ {
+ Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
+ }
+ else
+ {
+ Device.SetVolume(0);
+ }
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp))
+ {
+ GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown))
+ {
+ GraphicsConfig.ResScale =
+ (MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1;
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.VolumeUp) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.VolumeUp))
+ {
+ _newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2);
+ Device.SetVolume(_newVolume);
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.VolumeDown) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.VolumeDown))
+ {
+ _newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2);
+ Device.SetVolume(_newVolume);
+ }
+
+ _prevHotkeyState = currentHotkeyState;
+ }
+
+ // Touchscreen
+ bool hasTouch = false;
+
+ // Get screen touch position
+ if ((Toplevel as MainWindow).IsFocused && !ConfigurationState.Instance.Hid.EnableMouse)
+ {
+ hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as GTK3MouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
+ }
+
+ if (!hasTouch)
+ {
+ TouchScreenManager.Update(false);
+ }
+
+ Device.Hid.DebugPad.Update();
+
+ return true;
+ }
+
+ [Flags]
+ private enum KeyboardHotkeyState
+ {
+ None = 0,
+ ToggleVSync = 1 << 0,
+ Screenshot = 1 << 1,
+ ShowUI = 1 << 2,
+ Pause = 1 << 3,
+ ToggleMute = 1 << 4,
+ ResScaleUp = 1 << 5,
+ ResScaleDown = 1 << 6,
+ VolumeUp = 1 << 7,
+ VolumeDown = 1 << 8,
+ }
+
+ private KeyboardHotkeyState GetHotkeyState()
+ {
+ KeyboardHotkeyState state = KeyboardHotkeyState.None;
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
+ {
+ state |= KeyboardHotkeyState.ToggleVSync;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
+ {
+ state |= KeyboardHotkeyState.Screenshot;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUI))
+ {
+ state |= KeyboardHotkeyState.ShowUI;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause))
+ {
+ state |= KeyboardHotkeyState.Pause;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute))
+ {
+ state |= KeyboardHotkeyState.ToggleMute;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp))
+ {
+ state |= KeyboardHotkeyState.ResScaleUp;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown))
+ {
+ state |= KeyboardHotkeyState.ResScaleDown;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeUp))
+ {
+ state |= KeyboardHotkeyState.VolumeUp;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeDown))
+ {
+ state |= KeyboardHotkeyState.VolumeDown;
+ }
+
+ return state;
+ }
+ }
+}