From 28618c58d7ee1ae63fc57deca791a64ab38b57af Mon Sep 17 00:00:00 2001 From: emmauss Date: Mon, 28 Jun 2021 20:09:43 +0000 Subject: [PATCH] Add Screenshot Feature (#2354) * Add internal screenshot capabilities * update version notice --- .../Configuration/Hid/KeyboardHotkeys.cs | 1 + Ryujinx.Graphics.GAL/IRenderer.cs | 4 + .../ScreenCaptureImageInfo.cs | 22 +++++ Ryujinx.Graphics.OpenGL/Renderer.cs | 12 +++ Ryujinx.Graphics.OpenGL/Window.cs | 19 +++++ Ryujinx/Config.json | 5 +- .../Configuration/ConfigurationFileFormat.cs | 2 +- Ryujinx/Configuration/ConfigurationState.cs | 16 +++- Ryujinx/Ui/MainWindow.cs | 30 +++++-- Ryujinx/Ui/MainWindow.glade | 83 ++++++++++--------- Ryujinx/Ui/RendererWidgetBase.cs | 73 +++++++++++++++- Ryujinx/_schema.json | 9 +- 12 files changed, 220 insertions(+), 56 deletions(-) create mode 100644 Ryujinx.Graphics.GAL/ScreenCaptureImageInfo.cs diff --git a/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index 8e9f168d8..47772ea84 100644 --- a/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -3,5 +3,6 @@ public struct KeyboardHotkeys { public Key ToggleVsync { get; set; } + public Key Screenshot { get; set; } } } \ No newline at end of file diff --git a/Ryujinx.Graphics.GAL/IRenderer.cs b/Ryujinx.Graphics.GAL/IRenderer.cs index d03cb4c01..18f10915b 100644 --- a/Ryujinx.Graphics.GAL/IRenderer.cs +++ b/Ryujinx.Graphics.GAL/IRenderer.cs @@ -6,6 +6,8 @@ namespace Ryujinx.Graphics.GAL { public interface IRenderer : IDisposable { + event EventHandler ScreenCaptured; + IPipeline Pipeline { get; } IWindow Window { get; } @@ -44,5 +46,7 @@ namespace Ryujinx.Graphics.GAL void WaitSync(ulong id); void Initialize(GraphicsDebugLevel logLevel); + + void Screenshot(); } } diff --git a/Ryujinx.Graphics.GAL/ScreenCaptureImageInfo.cs b/Ryujinx.Graphics.GAL/ScreenCaptureImageInfo.cs new file mode 100644 index 000000000..227d64b69 --- /dev/null +++ b/Ryujinx.Graphics.GAL/ScreenCaptureImageInfo.cs @@ -0,0 +1,22 @@ +namespace Ryujinx.Graphics.GAL +{ + public struct ScreenCaptureImageInfo + { + public ScreenCaptureImageInfo(int width, int height, bool isBgra, byte[] data, bool flipX, bool flipY) + { + Width = width; + Height = height; + IsBgra = isBgra; + Data = data; + FlipX = flipX; + FlipY = flipY; + } + + public int Width { get; } + public int Height { get; } + public byte[] Data { get; } + public bool IsBgra { get; } + public bool FlipX { get; } + public bool FlipY { get; } + } +} \ No newline at end of file diff --git a/Ryujinx.Graphics.OpenGL/Renderer.cs b/Ryujinx.Graphics.OpenGL/Renderer.cs index a2be43730..001cac8d6 100644 --- a/Ryujinx.Graphics.OpenGL/Renderer.cs +++ b/Ryujinx.Graphics.OpenGL/Renderer.cs @@ -28,6 +28,8 @@ namespace Ryujinx.Graphics.OpenGL private Sync _sync; + public event EventHandler ScreenCaptured; + internal ResourcePool ResourcePool { get; } internal int BufferCount { get; private set; } @@ -196,5 +198,15 @@ namespace Ryujinx.Graphics.OpenGL { _sync.Wait(id); } + + public void Screenshot() + { + _window.ScreenCaptureRequested = true; + } + + public void OnScreenCaptured(ScreenCaptureImageInfo bitmap) + { + ScreenCaptured?.Invoke(this, bitmap); + } } } diff --git a/Ryujinx.Graphics.OpenGL/Window.cs b/Ryujinx.Graphics.OpenGL/Window.cs index b7525ae51..35b04d6d9 100644 --- a/Ryujinx.Graphics.OpenGL/Window.cs +++ b/Ryujinx.Graphics.OpenGL/Window.cs @@ -16,6 +16,8 @@ namespace Ryujinx.Graphics.OpenGL internal BackgroundContextWorker BackgroundContext { get; private set; } + internal bool ScreenCaptureRequested { get; set; } + public Window(Renderer renderer) { _renderer = renderer; @@ -106,6 +108,13 @@ namespace Ryujinx.Graphics.OpenGL int dstY0 = crop.FlipY ? dstPaddingY : _height - dstPaddingY; int dstY1 = crop.FlipY ? _height - dstPaddingY : dstPaddingY; + if (ScreenCaptureRequested) + { + CaptureFrame(srcX0, srcY0, srcX1, srcY1, view.Format.IsBgra8(), crop.FlipX, crop.FlipY); + + ScreenCaptureRequested = false; + } + GL.BlitFramebuffer( srcX0, srcY0, @@ -159,6 +168,16 @@ namespace Ryujinx.Graphics.OpenGL BackgroundContext = new BackgroundContextWorker(baseContext); } + public void CaptureFrame(int x, int y, int width, int height, bool isBgra, bool flipX, bool flipY) + { + long size = Math.Abs(4 * width * height); + byte[] bitmap = new byte[size]; + + GL.ReadPixels(x, y, width, height, isBgra ? PixelFormat.Bgra : PixelFormat.Rgba, PixelType.UnsignedByte, bitmap); + + _renderer.OnScreenCaptured(new ScreenCaptureImageInfo(width, height, isBgra, bitmap, flipX, flipY)); + } + public void Dispose() { BackgroundContext.Dispose(); diff --git a/Ryujinx/Config.json b/Ryujinx/Config.json index 033186fe4..3d4ea23b1 100644 --- a/Ryujinx/Config.json +++ b/Ryujinx/Config.json @@ -1,5 +1,5 @@ { - "version": 27, + "version": 28, "enable_file_log": true, "res_scale": 1, "res_scale_custom": 1, @@ -57,7 +57,8 @@ "enable_keyboard": false, "enable_mouse": false, "hotkeys": { - "toggle_vsync": "Tab" + "toggle_vsync": "Tab", + "screenshot": "F8" }, "keyboard_config": [], "controller_config": [], diff --git a/Ryujinx/Configuration/ConfigurationFileFormat.cs b/Ryujinx/Configuration/ConfigurationFileFormat.cs index 04a518155..4634dafeb 100644 --- a/Ryujinx/Configuration/ConfigurationFileFormat.cs +++ b/Ryujinx/Configuration/ConfigurationFileFormat.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 27; + public const int CurrentVersion = 28; public int Version { get; set; } diff --git a/Ryujinx/Configuration/ConfigurationState.cs b/Ryujinx/Configuration/ConfigurationState.cs index 1769dfa9e..c98fbcca2 100644 --- a/Ryujinx/Configuration/ConfigurationState.cs +++ b/Ryujinx/Configuration/ConfigurationState.cs @@ -542,7 +542,8 @@ namespace Ryujinx.Configuration Hid.EnableMouse.Value = false; Hid.Hotkeys.Value = new KeyboardHotkeys { - ToggleVsync = Key.Tab + ToggleVsync = Key.Tab, + Screenshot = Key.F8 }; Hid.InputConfig.Value = new List { @@ -845,6 +846,19 @@ namespace Ryujinx.Configuration configurationFileUpdated = true; } + if (configurationFileFormat.Version < 28) + { + Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = Key.Tab, + Screenshot = Key.F8 + }; + + configurationFileUpdated = true; + } + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 113ac6398..eb1e10b3a 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -1,10 +1,21 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + using ARMeilleure.Translation; using ARMeilleure.Translation.PTC; + using Gtk; + using LibHac.Common; using LibHac.FsSystem; using LibHac.FsSystem.NcaUtils; using LibHac.Ns; + using Ryujinx.Audio.Backends.Dummy; using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.SDL2; @@ -31,13 +42,6 @@ using Ryujinx.Ui.Applet; using Ryujinx.Ui.Helper; using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Windows; -using System; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; using GUI = Gtk.Builder.ObjectAttribute; @@ -96,6 +100,7 @@ namespace Ryujinx.Ui [GUI] MenuItem _stopEmulation; [GUI] MenuItem _simulateWakeUpMessage; [GUI] MenuItem _scanAmiibo; + [GUI] MenuItem _takeScreenshot; [GUI] MenuItem _fullScreen; [GUI] CheckMenuItem _startFullScreen; [GUI] CheckMenuItem _favToggle; @@ -1377,7 +1382,8 @@ namespace Ryujinx.Ui private void ActionMenu_StateChanged(object o, StateChangedArgs args) { - _scanAmiibo.Sensitive = _emulationContext != null && _emulationContext.System.SearchingForAmiibo(out int _); + _scanAmiibo.Sensitive = _emulationContext != null && _emulationContext.System.SearchingForAmiibo(out int _); + _takeScreenshot.Sensitive = _emulationContext != null; } private void Scan_Amiibo(object sender, EventArgs args) @@ -1402,6 +1408,14 @@ namespace Ryujinx.Ui } } + private void Take_Screenshot(object sender, EventArgs args) + { + if (_emulationContext != null && RendererWidget != null) + { + RendererWidget.ScreenshotRequested = true; + } + } + private void AmiiboWindow_DeleteEvent(object sender, DeleteEventArgs args) { if (((AmiiboWindow)sender).AmiiboId != "" && ((AmiiboWindow)sender).Response == ResponseType.Ok) diff --git a/Ryujinx/Ui/MainWindow.glade b/Ryujinx/Ui/MainWindow.glade index e974d878c..7bf38f473 100644 --- a/Ryujinx/Ui/MainWindow.glade +++ b/Ryujinx/Ui/MainWindow.glade @@ -1,14 +1,11 @@ - + False Ryujinx center - - - True @@ -332,6 +329,15 @@ + + + True + False + Take a screenshot + Take Screenshot + + + @@ -450,7 +456,7 @@ True True True - + @@ -484,7 +490,7 @@ True False 5 - + RefreshList @@ -547,8 +553,7 @@ True False - 0 - + True @@ -581,8 +586,7 @@ True False - 0 - + True @@ -614,8 +618,7 @@ True False - 0 - + True @@ -713,35 +716,6 @@ True 1 - - - - False - 5 - 5 - 0/0 - False - - - False - True - 11 - - - - - 200 - False - 5 - 5 - 6 - False - - - False - True - 12 - @@ -783,6 +757,33 @@ 4 + + + False + 5 + 5 + 0/0 + + + False + True + 11 + + + + + 200 + False + 5 + 5 + 6 + + + False + True + 12 + + False diff --git a/Ryujinx/Ui/RendererWidgetBase.cs b/Ryujinx/Ui/RendererWidgetBase.cs index 699f06c51..dee5cbb6c 100644 --- a/Ryujinx/Ui/RendererWidgetBase.cs +++ b/Ryujinx/Ui/RendererWidgetBase.cs @@ -4,6 +4,7 @@ using Gdk; using Gtk; using Ryujinx.Common; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; using Ryujinx.Configuration; using Ryujinx.Graphics.GAL; using Ryujinx.HLE.HOS.Services.Hid; @@ -11,13 +12,19 @@ using Ryujinx.Input; using Ryujinx.Input.GTK3; using Ryujinx.Input.HLE; 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.Linq; +using System.IO; using System.Threading; +using System.Threading.Tasks; namespace Ryujinx.Ui { + using Image = SixLabors.ImageSharp.Image; using Key = Input.Key; using Switch = HLE.Switch; @@ -33,6 +40,8 @@ namespace Ryujinx.Ui public Switch Device { get; private set; } public IRenderer Renderer { get; private set; } + public bool ScreenshotRequested { get; set; } + public static event EventHandler StatusUpdatedEvent; private bool _isActive; @@ -290,10 +299,56 @@ namespace Ryujinx.Ui Renderer = Device.Gpu.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) + { + 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 = System.IO.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyPictures), "Ryujinx"); + string path = System.IO.Path.Combine(directory, filename); + + Directory.CreateDirectory(directory); + + Image image = e.IsBgra ? Image.LoadPixelData(e.Data, e.Width, e.Height) + : Image.LoadPixelData(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"); + } + }); + } + } + public void Render() { Gtk.Window parent = Toplevel as Gtk.Window; @@ -490,6 +545,14 @@ namespace Ryujinx.Ui Device.EnableDeviceVsync = !Device.EnableDeviceVsync; } + if ((currentHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot) && + !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot)) || ScreenshotRequested) + { + ScreenshotRequested = false; + + Renderer.Screenshot(); + } + _prevHotkeyState = currentHotkeyState; } @@ -516,7 +579,8 @@ namespace Ryujinx.Ui private enum KeyboardHotkeyState { None, - ToggleVSync + ToggleVSync, + Screenshot } private KeyboardHotkeyState GetHotkeyState() @@ -527,6 +591,11 @@ namespace Ryujinx.Ui { state |= KeyboardHotkeyState.ToggleVSync; } + + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot)) + { + state |= KeyboardHotkeyState.Screenshot; + } return state; } diff --git a/Ryujinx/_schema.json b/Ryujinx/_schema.json index f819b9d6c..47da28b1c 100644 --- a/Ryujinx/_schema.json +++ b/Ryujinx/_schema.json @@ -1455,7 +1455,8 @@ "type": "object", "title": "Hotkey Controls", "required": [ - "toggle_vsync" + "toggle_vsync", + "screenshot" ], "properties": { "toggle_vsync": { @@ -1463,6 +1464,12 @@ "$ref": "#/definitions/key", "title": "Toggle VSync", "default": "Tab" + }, + "screenshot": { + "$id": "#/properties/hotkeys/properties/screenshot", + "$ref": "#/definitions/key", + "title": "Screenshot", + "default": "F8" } } },