RendererWidgetBase.cs
1 using Gdk; 2 using Gtk; 3 using Ryujinx.Common; 4 using Ryujinx.Common.Configuration; 5 using Ryujinx.Common.Logging; 6 using Ryujinx.Common.Utilities; 7 using Ryujinx.Graphics.GAL; 8 using Ryujinx.Graphics.GAL.Multithreading; 9 using Ryujinx.Graphics.Gpu; 10 using Ryujinx.Input; 11 using Ryujinx.Input.GTK3; 12 using Ryujinx.Input.HLE; 13 using Ryujinx.UI.Common.Configuration; 14 using Ryujinx.UI.Common.Helper; 15 using Ryujinx.UI.Widgets; 16 using SkiaSharp; 17 using System; 18 using System.Diagnostics; 19 using System.IO; 20 using System.Runtime.InteropServices; 21 using System.Threading; 22 using System.Threading.Tasks; 23 using Key = Ryujinx.Input.Key; 24 using ScalingFilter = Ryujinx.Graphics.GAL.ScalingFilter; 25 using Switch = Ryujinx.HLE.Switch; 26 27 namespace Ryujinx.UI 28 { 29 public abstract class RendererWidgetBase : DrawingArea 30 { 31 private const int SwitchPanelWidth = 1280; 32 private const int SwitchPanelHeight = 720; 33 private const int TargetFps = 60; 34 private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping. 35 private const float VolumeDelta = 0.05f; 36 37 public ManualResetEvent WaitEvent { get; set; } 38 public NpadManager NpadManager { get; } 39 public TouchScreenManager TouchScreenManager { get; } 40 public Switch Device { get; private set; } 41 public IRenderer Renderer { get; private set; } 42 43 public bool ScreenshotRequested { get; set; } 44 protected int WindowWidth { get; private set; } 45 protected int WindowHeight { get; private set; } 46 47 public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent; 48 49 private bool _isActive; 50 private bool _isStopped; 51 52 private bool _toggleFullscreen; 53 private bool _toggleDockedMode; 54 55 private readonly long _ticksPerFrame; 56 57 private long _ticks = 0; 58 private float _newVolume; 59 60 private readonly Stopwatch _chrono; 61 62 private KeyboardHotkeyState _prevHotkeyState; 63 64 private readonly ManualResetEvent _exitEvent; 65 private readonly ManualResetEvent _gpuDoneEvent; 66 67 private readonly CancellationTokenSource _gpuCancellationTokenSource; 68 69 // Hide Cursor 70 const int CursorHideIdleTime = 5; // seconds 71 private static readonly Cursor _invisibleCursor = new(Display.Default, CursorType.BlankCursor); 72 private long _lastCursorMoveTime; 73 private HideCursorMode _hideCursorMode; 74 private readonly InputManager _inputManager; 75 private readonly IKeyboard _keyboardInterface; 76 private readonly GraphicsDebugLevel _glLogLevel; 77 private string _gpuBackendName; 78 private string _gpuDriverName; 79 private bool _isMouseInClient; 80 81 public RendererWidgetBase(InputManager inputManager, GraphicsDebugLevel glLogLevel) 82 { 83 var mouseDriver = new GTK3MouseDriver(this); 84 85 _inputManager = inputManager; 86 _inputManager.SetMouseDriver(mouseDriver); 87 NpadManager = _inputManager.CreateNpadManager(); 88 TouchScreenManager = _inputManager.CreateTouchScreenManager(); 89 _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0"); 90 91 WaitEvent = new ManualResetEvent(false); 92 93 _glLogLevel = glLogLevel; 94 95 Destroyed += Renderer_Destroyed; 96 97 _chrono = new Stopwatch(); 98 99 _ticksPerFrame = Stopwatch.Frequency / TargetFps; 100 101 AddEvents((int)(EventMask.ButtonPressMask 102 | EventMask.ButtonReleaseMask 103 | EventMask.PointerMotionMask 104 | EventMask.ScrollMask 105 | EventMask.EnterNotifyMask 106 | EventMask.LeaveNotifyMask 107 | EventMask.KeyPressMask 108 | EventMask.KeyReleaseMask)); 109 110 _exitEvent = new ManualResetEvent(false); 111 _gpuDoneEvent = new ManualResetEvent(false); 112 113 _gpuCancellationTokenSource = new CancellationTokenSource(); 114 115 _hideCursorMode = ConfigurationState.Instance.HideCursor; 116 _lastCursorMoveTime = Stopwatch.GetTimestamp(); 117 118 ConfigurationState.Instance.HideCursor.Event += HideCursorStateChanged; 119 ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAnriAliasing; 120 ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter; 121 ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; 122 } 123 124 private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs<int> e) 125 { 126 Renderer.Window.SetScalingFilter((ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value); 127 Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value); 128 } 129 130 private void UpdateScalingFilter(object sender, ReactiveEventArgs<Ryujinx.Common.Configuration.ScalingFilter> e) 131 { 132 Renderer.Window.SetScalingFilter((ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value); 133 Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value); 134 } 135 136 public abstract void InitializeRenderer(); 137 138 public abstract void SwapBuffers(); 139 140 protected abstract string GetGpuBackendName(); 141 142 private string GetGpuDriverName() 143 { 144 return Renderer.GetHardwareInfo().GpuDriver; 145 } 146 147 private void HideCursorStateChanged(object sender, ReactiveEventArgs<HideCursorMode> state) 148 { 149 Application.Invoke(delegate 150 { 151 _hideCursorMode = state.NewValue; 152 153 switch (_hideCursorMode) 154 { 155 case HideCursorMode.Never: 156 Window.Cursor = null; 157 break; 158 case HideCursorMode.OnIdle: 159 _lastCursorMoveTime = Stopwatch.GetTimestamp(); 160 break; 161 case HideCursorMode.Always: 162 Window.Cursor = _invisibleCursor; 163 break; 164 default: 165 throw new ArgumentOutOfRangeException(nameof(state)); 166 } 167 }); 168 } 169 170 private void Renderer_Destroyed(object sender, EventArgs e) 171 { 172 ConfigurationState.Instance.HideCursor.Event -= HideCursorStateChanged; 173 ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAnriAliasing; 174 ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter; 175 ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel; 176 177 NpadManager.Dispose(); 178 Dispose(); 179 } 180 181 private void UpdateAnriAliasing(object sender, ReactiveEventArgs<Ryujinx.Common.Configuration.AntiAliasing> e) 182 { 183 Renderer?.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)e.NewValue); 184 } 185 186 protected override bool OnMotionNotifyEvent(EventMotion evnt) 187 { 188 if (_hideCursorMode == HideCursorMode.OnIdle) 189 { 190 _lastCursorMoveTime = Stopwatch.GetTimestamp(); 191 } 192 193 if (ConfigurationState.Instance.Hid.EnableMouse) 194 { 195 Window.Cursor = _invisibleCursor; 196 } 197 198 _isMouseInClient = true; 199 200 return false; 201 } 202 203 protected override bool OnEnterNotifyEvent(EventCrossing evnt) 204 { 205 Window.Cursor = ConfigurationState.Instance.Hid.EnableMouse ? _invisibleCursor : null; 206 207 _isMouseInClient = true; 208 209 return base.OnEnterNotifyEvent(evnt); 210 } 211 212 protected override bool OnLeaveNotifyEvent(EventCrossing evnt) 213 { 214 Window.Cursor = null; 215 216 _isMouseInClient = false; 217 218 return base.OnLeaveNotifyEvent(evnt); 219 } 220 221 protected override void OnGetPreferredHeight(out int minimumHeight, out int naturalHeight) 222 { 223 Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window); 224 225 // If the monitor is at least 1080p, use the Switch panel size as minimal size. 226 if (monitor.Geometry.Height >= 1080) 227 { 228 minimumHeight = SwitchPanelHeight; 229 } 230 // Otherwise, we default minimal size to 480p 16:9. 231 else 232 { 233 minimumHeight = 480; 234 } 235 236 naturalHeight = minimumHeight; 237 } 238 239 protected override void OnGetPreferredWidth(out int minimumWidth, out int naturalWidth) 240 { 241 Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window); 242 243 // If the monitor is at least 1080p, use the Switch panel size as minimal size. 244 if (monitor.Geometry.Height >= 1080) 245 { 246 minimumWidth = SwitchPanelWidth; 247 } 248 // Otherwise, we default minimal size to 480p 16:9. 249 else 250 { 251 minimumWidth = 854; 252 } 253 254 naturalWidth = minimumWidth; 255 } 256 257 protected override bool OnConfigureEvent(EventConfigure evnt) 258 { 259 bool result = base.OnConfigureEvent(evnt); 260 261 Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window); 262 263 WindowWidth = evnt.Width * monitor.ScaleFactor; 264 WindowHeight = evnt.Height * monitor.ScaleFactor; 265 266 Renderer?.Window?.SetSize(WindowWidth, WindowHeight); 267 268 return result; 269 } 270 271 private void HandleScreenState(KeyboardStateSnapshot keyboard) 272 { 273 bool toggleFullscreen = keyboard.IsPressed(Key.F11) 274 || ((keyboard.IsPressed(Key.AltLeft) 275 || keyboard.IsPressed(Key.AltRight)) 276 && keyboard.IsPressed(Key.Enter)) 277 || keyboard.IsPressed(Key.Escape); 278 279 bool fullScreenToggled = ParentWindow.State.HasFlag(WindowState.Fullscreen); 280 281 if (toggleFullscreen != _toggleFullscreen) 282 { 283 if (toggleFullscreen) 284 { 285 if (fullScreenToggled) 286 { 287 ParentWindow.Unfullscreen(); 288 (Toplevel as MainWindow)?.ToggleExtraWidgets(true); 289 } 290 else 291 { 292 if (keyboard.IsPressed(Key.Escape)) 293 { 294 if (!ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog()) 295 { 296 Exit(); 297 } 298 } 299 else 300 { 301 ParentWindow.Fullscreen(); 302 (Toplevel as MainWindow)?.ToggleExtraWidgets(false); 303 } 304 } 305 } 306 } 307 308 _toggleFullscreen = toggleFullscreen; 309 310 bool toggleDockedMode = keyboard.IsPressed(Key.F9); 311 312 if (toggleDockedMode != _toggleDockedMode) 313 { 314 if (toggleDockedMode) 315 { 316 ConfigurationState.Instance.System.EnableDockedMode.Value = 317 !ConfigurationState.Instance.System.EnableDockedMode.Value; 318 } 319 } 320 321 _toggleDockedMode = toggleDockedMode; 322 323 if (_isMouseInClient) 324 { 325 if (ConfigurationState.Instance.Hid.EnableMouse.Value) 326 { 327 Window.Cursor = _invisibleCursor; 328 } 329 else 330 { 331 switch (_hideCursorMode) 332 { 333 case HideCursorMode.OnIdle: 334 long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime; 335 Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null; 336 break; 337 case HideCursorMode.Always: 338 Window.Cursor = _invisibleCursor; 339 break; 340 case HideCursorMode.Never: 341 Window.Cursor = null; 342 break; 343 } 344 } 345 } 346 } 347 348 public void Initialize(Switch device) 349 { 350 Device = device; 351 352 IRenderer renderer = Device.Gpu.Renderer; 353 354 if (renderer is ThreadedRenderer tr) 355 { 356 renderer = tr.BaseRenderer; 357 } 358 359 Renderer = renderer; 360 Renderer?.Window?.SetSize(WindowWidth, WindowHeight); 361 362 if (Renderer != null) 363 { 364 Renderer.ScreenCaptured += Renderer_ScreenCaptured; 365 } 366 367 NpadManager.Initialize(device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); 368 TouchScreenManager.Initialize(device); 369 } 370 371 private unsafe void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e) 372 { 373 if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0) 374 { 375 Task.Run(() => 376 { 377 lock (this) 378 { 379 string applicationName = Device.Processes.ActiveApplication.Name; 380 string sanitizedApplicationName = FileSystemUtils.SanitizeFileName(applicationName); 381 DateTime currentTime = DateTime.Now; 382 383 string filename = $"{sanitizedApplicationName}_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png"; 384 385 string directory = AppDataManager.Mode switch 386 { 387 AppDataManager.LaunchMode.Portable or AppDataManager.LaunchMode.Custom => System.IO.Path.Combine(AppDataManager.BaseDirPath, "screenshots"), 388 _ => System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx"), 389 }; 390 391 string path = System.IO.Path.Combine(directory, filename); 392 393 try 394 { 395 Directory.CreateDirectory(directory); 396 } 397 catch (Exception ex) 398 { 399 Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {directory}. Error : {ex.GetType().Name}", "Screenshot"); 400 401 return; 402 } 403 404 var colorType = e.IsBgra ? SKColorType.Bgra8888 : SKColorType.Rgba8888; 405 using var image = new SKBitmap(new SKImageInfo(e.Width, e.Height, colorType, SKAlphaType.Premul)); 406 407 Marshal.Copy(e.Data, 0, image.GetPixels(), e.Data.Length); 408 using var surface = SKSurface.Create(image.Info); 409 var canvas = surface.Canvas; 410 411 if (e.FlipX || e.FlipY) 412 { 413 canvas.Clear(SKColors.Transparent); 414 415 float scaleX = e.FlipX ? -1 : 1; 416 float scaleY = e.FlipY ? -1 : 1; 417 418 var matrix = SKMatrix.CreateScale(scaleX, scaleY, image.Width / 2f, image.Height / 2f); 419 420 canvas.SetMatrix(matrix); 421 } 422 canvas.DrawBitmap(image, new SKPoint()); 423 424 surface.Flush(); 425 using var snapshot = surface.Snapshot(); 426 using var encoded = snapshot.Encode(SKEncodedImageFormat.Png, 80); 427 using var file = File.OpenWrite(path); 428 encoded.SaveTo(file); 429 430 image.Dispose(); 431 432 Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot"); 433 } 434 }); 435 } 436 else 437 { 438 Logger.Error?.Print(LogClass.Application, $"Screenshot is empty. Size : {e.Data.Length} bytes. Resolution : {e.Width}x{e.Height}", "Screenshot"); 439 } 440 } 441 442 public void Render() 443 { 444 Gtk.Window parent = Toplevel as Gtk.Window; 445 parent.Present(); 446 447 InitializeRenderer(); 448 449 Device.Gpu.Renderer.Initialize(_glLogLevel); 450 451 Renderer.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)ConfigurationState.Instance.Graphics.AntiAliasing.Value); 452 Renderer.Window.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value); 453 Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value); 454 455 _gpuBackendName = GetGpuBackendName(); 456 _gpuDriverName = GetGpuDriverName(); 457 458 Device.Gpu.Renderer.RunLoop(() => 459 { 460 Device.Gpu.SetGpuThread(); 461 Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); 462 463 Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); 464 465 (Toplevel as MainWindow)?.ActivatePauseMenu(); 466 467 while (_isActive) 468 { 469 if (_isStopped) 470 { 471 return; 472 } 473 474 _ticks += _chrono.ElapsedTicks; 475 476 _chrono.Restart(); 477 478 if (Device.WaitFifo()) 479 { 480 Device.Statistics.RecordFifoStart(); 481 Device.ProcessFrame(); 482 Device.Statistics.RecordFifoEnd(); 483 } 484 485 while (Device.ConsumeFrameAvailable()) 486 { 487 Device.PresentFrame(SwapBuffers); 488 } 489 490 if (_ticks >= _ticksPerFrame) 491 { 492 string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? "Docked" : "Handheld"; 493 float scale = GraphicsConfig.ResScale; 494 if (scale != 1) 495 { 496 dockedMode += $" ({scale}x)"; 497 } 498 499 StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( 500 Device.EnableDeviceVsync, 501 Device.GetVolume(), 502 _gpuBackendName, 503 dockedMode, 504 ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(), 505 $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", 506 $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %", 507 $"GPU: {_gpuDriverName}")); 508 509 _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame); 510 } 511 } 512 513 // Make sure all commands in the run loop are fully executed before leaving the loop. 514 if (Device.Gpu.Renderer is ThreadedRenderer threaded) 515 { 516 threaded.FlushThreadedCommands(); 517 } 518 519 _gpuDoneEvent.Set(); 520 }); 521 } 522 523 public void Start() 524 { 525 _chrono.Restart(); 526 527 _isActive = true; 528 529 Gtk.Window parent = Toplevel as Gtk.Window; 530 531 Application.Invoke(delegate 532 { 533 parent.Present(); 534 535 var activeProcess = Device.Processes.ActiveApplication; 536 537 parent.Title = TitleHelper.ActiveApplicationTitle(activeProcess, Program.Version); 538 }); 539 540 Thread renderLoopThread = new(Render) 541 { 542 Name = "GUI.RenderLoop", 543 }; 544 renderLoopThread.Start(); 545 546 Thread nvidiaStutterWorkaround = null; 547 if (Renderer is Graphics.OpenGL.OpenGLRenderer) 548 { 549 nvidiaStutterWorkaround = new Thread(NvidiaStutterWorkaround) 550 { 551 Name = "GUI.NvidiaStutterWorkaround", 552 }; 553 nvidiaStutterWorkaround.Start(); 554 } 555 556 MainLoop(); 557 558 // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose. 559 // We only need to wait for all commands submitted during the main gpu loop to be processed. 560 _gpuDoneEvent.WaitOne(); 561 _gpuDoneEvent.Dispose(); 562 nvidiaStutterWorkaround?.Join(); 563 564 Exit(); 565 } 566 567 public void Exit() 568 { 569 TouchScreenManager?.Dispose(); 570 NpadManager?.Dispose(); 571 572 if (_isStopped) 573 { 574 return; 575 } 576 577 _gpuCancellationTokenSource.Cancel(); 578 579 _isStopped = true; 580 581 if (_isActive) 582 { 583 _isActive = false; 584 585 _exitEvent.WaitOne(); 586 _exitEvent.Dispose(); 587 } 588 } 589 590 private void NvidiaStutterWorkaround() 591 { 592 while (_isActive) 593 { 594 // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones. 595 // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity. 596 // However, it immediately starts up again, since the rules regarding when to terminate and when to start differ. 597 // This creates a new thread every second or so. 598 // The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics. 599 // This is a little over budget on a frame time of 16ms, so creates a large stutter. 600 // The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread. 601 602 // TODO: This should be removed when the issue with the GateThread is resolved. 603 604 ThreadPool.QueueUserWorkItem((state) => { }); 605 Thread.Sleep(300); 606 } 607 } 608 609 public void MainLoop() 610 { 611 while (_isActive) 612 { 613 UpdateFrame(); 614 615 // Polling becomes expensive if it's not slept 616 Thread.Sleep(1); 617 } 618 619 _exitEvent.Set(); 620 } 621 622 private bool UpdateFrame() 623 { 624 if (!_isActive) 625 { 626 return true; 627 } 628 629 if (_isStopped) 630 { 631 return false; 632 } 633 634 if ((Toplevel as MainWindow).IsFocused) 635 { 636 Application.Invoke(delegate 637 { 638 KeyboardStateSnapshot keyboard = _keyboardInterface.GetKeyboardStateSnapshot(); 639 640 HandleScreenState(keyboard); 641 642 if (keyboard.IsPressed(Key.Delete)) 643 { 644 if (!ParentWindow.State.HasFlag(WindowState.Fullscreen)) 645 { 646 Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel(); 647 } 648 } 649 }); 650 } 651 652 NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); 653 654 if ((Toplevel as MainWindow).IsFocused) 655 { 656 KeyboardHotkeyState currentHotkeyState = GetHotkeyState(); 657 658 if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync) && 659 !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync)) 660 { 661 Device.EnableDeviceVsync = !Device.EnableDeviceVsync; 662 } 663 664 if ((currentHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot) && 665 !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot)) || ScreenshotRequested) 666 { 667 ScreenshotRequested = false; 668 669 Renderer.Screenshot(); 670 } 671 672 if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ShowUI) && 673 !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ShowUI)) 674 { 675 (Toplevel as MainWindow).ToggleExtraWidgets(true); 676 } 677 678 if (currentHotkeyState.HasFlag(KeyboardHotkeyState.Pause) && 679 !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Pause)) 680 { 681 (Toplevel as MainWindow)?.TogglePause(); 682 } 683 684 if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute) && 685 !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute)) 686 { 687 if (Device.IsAudioMuted()) 688 { 689 Device.SetVolume(ConfigurationState.Instance.System.AudioVolume); 690 } 691 else 692 { 693 Device.SetVolume(0); 694 } 695 } 696 697 if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp) && 698 !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp)) 699 { 700 GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1; 701 } 702 703 if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown) && 704 !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown)) 705 { 706 GraphicsConfig.ResScale = 707 (MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1; 708 } 709 710 if (currentHotkeyState.HasFlag(KeyboardHotkeyState.VolumeUp) && 711 !_prevHotkeyState.HasFlag(KeyboardHotkeyState.VolumeUp)) 712 { 713 _newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2); 714 Device.SetVolume(_newVolume); 715 } 716 717 if (currentHotkeyState.HasFlag(KeyboardHotkeyState.VolumeDown) && 718 !_prevHotkeyState.HasFlag(KeyboardHotkeyState.VolumeDown)) 719 { 720 _newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2); 721 Device.SetVolume(_newVolume); 722 } 723 724 _prevHotkeyState = currentHotkeyState; 725 } 726 727 // Touchscreen 728 bool hasTouch = false; 729 730 // Get screen touch position 731 if ((Toplevel as MainWindow).IsFocused && !ConfigurationState.Instance.Hid.EnableMouse) 732 { 733 hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as GTK3MouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); 734 } 735 736 if (!hasTouch) 737 { 738 TouchScreenManager.Update(false); 739 } 740 741 Device.Hid.DebugPad.Update(); 742 743 return true; 744 } 745 746 [Flags] 747 private enum KeyboardHotkeyState 748 { 749 None = 0, 750 ToggleVSync = 1 << 0, 751 Screenshot = 1 << 1, 752 ShowUI = 1 << 2, 753 Pause = 1 << 3, 754 ToggleMute = 1 << 4, 755 ResScaleUp = 1 << 5, 756 ResScaleDown = 1 << 6, 757 VolumeUp = 1 << 7, 758 VolumeDown = 1 << 8, 759 } 760 761 private KeyboardHotkeyState GetHotkeyState() 762 { 763 KeyboardHotkeyState state = KeyboardHotkeyState.None; 764 765 if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) 766 { 767 state |= KeyboardHotkeyState.ToggleVSync; 768 } 769 770 if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot)) 771 { 772 state |= KeyboardHotkeyState.Screenshot; 773 } 774 775 if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUI)) 776 { 777 state |= KeyboardHotkeyState.ShowUI; 778 } 779 780 if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause)) 781 { 782 state |= KeyboardHotkeyState.Pause; 783 } 784 785 if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute)) 786 { 787 state |= KeyboardHotkeyState.ToggleMute; 788 } 789 790 if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp)) 791 { 792 state |= KeyboardHotkeyState.ResScaleUp; 793 } 794 795 if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown)) 796 { 797 state |= KeyboardHotkeyState.ResScaleDown; 798 } 799 800 if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeUp)) 801 { 802 state |= KeyboardHotkeyState.VolumeUp; 803 } 804 805 if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeDown)) 806 { 807 state |= KeyboardHotkeyState.VolumeDown; 808 } 809 810 return state; 811 } 812 } 813 }