Add Screenshot Feature (#2354)

* Add internal screenshot  capabilities

* update version notice
This commit is contained in:
emmauss 2021-06-28 20:09:43 +00:00 committed by GitHub
parent a79b39b913
commit 28618c58d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 220 additions and 56 deletions

View file

@ -3,5 +3,6 @@
public struct KeyboardHotkeys public struct KeyboardHotkeys
{ {
public Key ToggleVsync { get; set; } public Key ToggleVsync { get; set; }
public Key Screenshot { get; set; }
} }
} }

View file

@ -6,6 +6,8 @@ namespace Ryujinx.Graphics.GAL
{ {
public interface IRenderer : IDisposable public interface IRenderer : IDisposable
{ {
event EventHandler<ScreenCaptureImageInfo> ScreenCaptured;
IPipeline Pipeline { get; } IPipeline Pipeline { get; }
IWindow Window { get; } IWindow Window { get; }
@ -44,5 +46,7 @@ namespace Ryujinx.Graphics.GAL
void WaitSync(ulong id); void WaitSync(ulong id);
void Initialize(GraphicsDebugLevel logLevel); void Initialize(GraphicsDebugLevel logLevel);
void Screenshot();
} }
} }

View file

@ -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; }
}
}

View file

@ -28,6 +28,8 @@ namespace Ryujinx.Graphics.OpenGL
private Sync _sync; private Sync _sync;
public event EventHandler<ScreenCaptureImageInfo> ScreenCaptured;
internal ResourcePool ResourcePool { get; } internal ResourcePool ResourcePool { get; }
internal int BufferCount { get; private set; } internal int BufferCount { get; private set; }
@ -196,5 +198,15 @@ namespace Ryujinx.Graphics.OpenGL
{ {
_sync.Wait(id); _sync.Wait(id);
} }
public void Screenshot()
{
_window.ScreenCaptureRequested = true;
}
public void OnScreenCaptured(ScreenCaptureImageInfo bitmap)
{
ScreenCaptured?.Invoke(this, bitmap);
}
} }
} }

View file

@ -16,6 +16,8 @@ namespace Ryujinx.Graphics.OpenGL
internal BackgroundContextWorker BackgroundContext { get; private set; } internal BackgroundContextWorker BackgroundContext { get; private set; }
internal bool ScreenCaptureRequested { get; set; }
public Window(Renderer renderer) public Window(Renderer renderer)
{ {
_renderer = renderer; _renderer = renderer;
@ -106,6 +108,13 @@ namespace Ryujinx.Graphics.OpenGL
int dstY0 = crop.FlipY ? dstPaddingY : _height - dstPaddingY; int dstY0 = crop.FlipY ? dstPaddingY : _height - dstPaddingY;
int dstY1 = crop.FlipY ? _height - dstPaddingY : 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( GL.BlitFramebuffer(
srcX0, srcX0,
srcY0, srcY0,
@ -159,6 +168,16 @@ namespace Ryujinx.Graphics.OpenGL
BackgroundContext = new BackgroundContextWorker(baseContext); 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() public void Dispose()
{ {
BackgroundContext.Dispose(); BackgroundContext.Dispose();

View file

@ -1,5 +1,5 @@
{ {
"version": 27, "version": 28,
"enable_file_log": true, "enable_file_log": true,
"res_scale": 1, "res_scale": 1,
"res_scale_custom": 1, "res_scale_custom": 1,
@ -57,7 +57,8 @@
"enable_keyboard": false, "enable_keyboard": false,
"enable_mouse": false, "enable_mouse": false,
"hotkeys": { "hotkeys": {
"toggle_vsync": "Tab" "toggle_vsync": "Tab",
"screenshot": "F8"
}, },
"keyboard_config": [], "keyboard_config": [],
"controller_config": [], "controller_config": [],

View file

@ -14,7 +14,7 @@ namespace Ryujinx.Configuration
/// <summary> /// <summary>
/// The current version of the file format /// The current version of the file format
/// </summary> /// </summary>
public const int CurrentVersion = 27; public const int CurrentVersion = 28;
public int Version { get; set; } public int Version { get; set; }

View file

@ -542,7 +542,8 @@ namespace Ryujinx.Configuration
Hid.EnableMouse.Value = false; Hid.EnableMouse.Value = false;
Hid.Hotkeys.Value = new KeyboardHotkeys Hid.Hotkeys.Value = new KeyboardHotkeys
{ {
ToggleVsync = Key.Tab ToggleVsync = Key.Tab,
Screenshot = Key.F8
}; };
Hid.InputConfig.Value = new List<InputConfig> Hid.InputConfig.Value = new List<InputConfig>
{ {
@ -845,6 +846,19 @@ namespace Ryujinx.Configuration
configurationFileUpdated = true; 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; Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScale.Value = configurationFileFormat.ResScale;
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;

View file

@ -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;
using ARMeilleure.Translation.PTC; using ARMeilleure.Translation.PTC;
using Gtk; using Gtk;
using LibHac.Common; using LibHac.Common;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.FsSystem.NcaUtils; using LibHac.FsSystem.NcaUtils;
using LibHac.Ns; using LibHac.Ns;
using Ryujinx.Audio.Backends.Dummy; using Ryujinx.Audio.Backends.Dummy;
using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2; using Ryujinx.Audio.Backends.SDL2;
@ -31,13 +42,6 @@ using Ryujinx.Ui.Applet;
using Ryujinx.Ui.Helper; using Ryujinx.Ui.Helper;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using Ryujinx.Ui.Windows; 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; using GUI = Gtk.Builder.ObjectAttribute;
@ -96,6 +100,7 @@ namespace Ryujinx.Ui
[GUI] MenuItem _stopEmulation; [GUI] MenuItem _stopEmulation;
[GUI] MenuItem _simulateWakeUpMessage; [GUI] MenuItem _simulateWakeUpMessage;
[GUI] MenuItem _scanAmiibo; [GUI] MenuItem _scanAmiibo;
[GUI] MenuItem _takeScreenshot;
[GUI] MenuItem _fullScreen; [GUI] MenuItem _fullScreen;
[GUI] CheckMenuItem _startFullScreen; [GUI] CheckMenuItem _startFullScreen;
[GUI] CheckMenuItem _favToggle; [GUI] CheckMenuItem _favToggle;
@ -1378,6 +1383,7 @@ namespace Ryujinx.Ui
private void ActionMenu_StateChanged(object o, StateChangedArgs args) 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) 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) private void AmiiboWindow_DeleteEvent(object sender, DeleteEventArgs args)
{ {
if (((AmiiboWindow)sender).AmiiboId != "" && ((AmiiboWindow)sender).Response == ResponseType.Ok) if (((AmiiboWindow)sender).AmiiboId != "" && ((AmiiboWindow)sender).Response == ResponseType.Ok)

View file

@ -1,14 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 --> <!-- Generated with glade 3.38.2 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk+" version="3.20"/>
<object class="GtkApplicationWindow" id="_mainWin"> <object class="GtkApplicationWindow" id="_mainWin">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">Ryujinx</property> <property name="title" translatable="yes">Ryujinx</property>
<property name="window_position">center</property> <property name="window_position">center</property>
<child type="titlebar">
<placeholder/>
</child>
<child> <child>
<object class="GtkBox" id="_box"> <object class="GtkBox" id="_box">
<property name="visible">True</property> <property name="visible">True</property>
@ -332,6 +329,15 @@
<signal name="activate" handler="Scan_Amiibo" swapped="no"/> <signal name="activate" handler="Scan_Amiibo" swapped="no"/>
</object> </object>
</child> </child>
<child>
<object class="GtkMenuItem" id="_takeScreenshot">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Take a screenshot</property>
<property name="label" translatable="yes">Take Screenshot</property>
<signal name="activate" handler="Take_Screenshot" swapped="no"/>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>
@ -450,7 +456,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="reorderable">True</property> <property name="reorderable">True</property>
<property name="hover_selection">True</property> <property name="hover_selection">True</property>
<signal name="row-activated" handler="Row_Activated" swapped="no"/> <signal name="row_activated" handler="Row_Activated" swapped="no"/>
<child internal-child="selection"> <child internal-child="selection">
<object class="GtkTreeSelection" id="_gameTableSelection"/> <object class="GtkTreeSelection" id="_gameTableSelection"/>
</child> </child>
@ -484,7 +490,7 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">5</property> <property name="margin_left">5</property>
<signal name="button-release-event" handler="RefreshList_Pressed" swapped="no"/> <signal name="button_release_event" handler="RefreshList_Pressed" swapped="no"/>
<child> <child>
<object class="GtkImage"> <object class="GtkImage">
<property name="name">RefreshList</property> <property name="name">RefreshList</property>
@ -547,8 +553,7 @@
<object class="GtkEventBox"> <object class="GtkEventBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">0</property> <signal name="button_release_event" handler="VSyncStatus_Clicked" swapped="no"/>
<signal name="button-release-event" handler="VSyncStatus_Clicked" swapped="no"/>
<child> <child>
<object class="GtkLabel" id="_vSyncStatus"> <object class="GtkLabel" id="_vSyncStatus">
<property name="visible">True</property> <property name="visible">True</property>
@ -581,8 +586,7 @@
<object class="GtkEventBox"> <object class="GtkEventBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">0</property> <signal name="button_release_event" handler="DockedMode_Clicked" swapped="no"/>
<signal name="button-release-event" handler="DockedMode_Clicked" swapped="no"/>
<child> <child>
<object class="GtkLabel" id="_dockedMode"> <object class="GtkLabel" id="_dockedMode">
<property name="visible">True</property> <property name="visible">True</property>
@ -614,8 +618,7 @@
<object class="GtkEventBox"> <object class="GtkEventBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">0</property> <signal name="button_release_event" handler="AspectRatio_Clicked" swapped="no"/>
<signal name="button-release-event" handler="AspectRatio_Clicked" swapped="no"/>
<child> <child>
<object class="GtkLabel" id="_aspectRatio"> <object class="GtkLabel" id="_aspectRatio">
<property name="visible">True</property> <property name="visible">True</property>
@ -713,35 +716,6 @@
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child>
<child>
<object class="GtkLabel" id="_loadingStatusLabel">
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="label" translatable="yes">0/0 </property>
<property name="visible">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">11</property>
</packing>
</child>
<child>
<object class="GtkProgressBar" id="_loadingStatusBar">
<property name="width_request">200</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">6</property>
<property name="visible">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">12</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
@ -783,6 +757,33 @@
<property name="position">4</property> <property name="position">4</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkLabel" id="_loadingStatusLabel">
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="label" translatable="yes">0/0 </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">11</property>
</packing>
</child>
<child>
<object class="GtkProgressBar" id="_loadingStatusBar">
<property name="width_request">200</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">12</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>

View file

@ -4,6 +4,7 @@ using Gdk;
using Gtk; using Gtk;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Configuration; using Ryujinx.Configuration;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
@ -11,13 +12,19 @@ using Ryujinx.Input;
using Ryujinx.Input.GTK3; using Ryujinx.Input.GTK3;
using Ryujinx.Input.HLE; using Ryujinx.Input.HLE;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ui namespace Ryujinx.Ui
{ {
using Image = SixLabors.ImageSharp.Image;
using Key = Input.Key; using Key = Input.Key;
using Switch = HLE.Switch; using Switch = HLE.Switch;
@ -33,6 +40,8 @@ namespace Ryujinx.Ui
public Switch Device { get; private set; } public Switch Device { get; private set; }
public IRenderer Renderer { get; private set; } public IRenderer Renderer { get; private set; }
public bool ScreenshotRequested { get; set; }
public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent; public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
private bool _isActive; private bool _isActive;
@ -290,10 +299,56 @@ namespace Ryujinx.Ui
Renderer = Device.Gpu.Renderer; Renderer = Device.Gpu.Renderer;
Renderer?.Window.SetSize(_windowWidth, _windowHeight); 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); NpadManager.Initialize(device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
TouchScreenManager.Initialize(device); 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<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");
}
});
}
}
public void Render() public void Render()
{ {
Gtk.Window parent = Toplevel as Gtk.Window; Gtk.Window parent = Toplevel as Gtk.Window;
@ -490,6 +545,14 @@ namespace Ryujinx.Ui
Device.EnableDeviceVsync = !Device.EnableDeviceVsync; Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
} }
if ((currentHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot) &&
!_prevHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot)) || ScreenshotRequested)
{
ScreenshotRequested = false;
Renderer.Screenshot();
}
_prevHotkeyState = currentHotkeyState; _prevHotkeyState = currentHotkeyState;
} }
@ -516,7 +579,8 @@ namespace Ryujinx.Ui
private enum KeyboardHotkeyState private enum KeyboardHotkeyState
{ {
None, None,
ToggleVSync ToggleVSync,
Screenshot
} }
private KeyboardHotkeyState GetHotkeyState() private KeyboardHotkeyState GetHotkeyState()
@ -528,6 +592,11 @@ namespace Ryujinx.Ui
state |= KeyboardHotkeyState.ToggleVSync; state |= KeyboardHotkeyState.ToggleVSync;
} }
if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
{
state |= KeyboardHotkeyState.Screenshot;
}
return state; return state;
} }
} }

View file

@ -1455,7 +1455,8 @@
"type": "object", "type": "object",
"title": "Hotkey Controls", "title": "Hotkey Controls",
"required": [ "required": [
"toggle_vsync" "toggle_vsync",
"screenshot"
], ],
"properties": { "properties": {
"toggle_vsync": { "toggle_vsync": {
@ -1463,6 +1464,12 @@
"$ref": "#/definitions/key", "$ref": "#/definitions/key",
"title": "Toggle VSync", "title": "Toggle VSync",
"default": "Tab" "default": "Tab"
},
"screenshot": {
"$id": "#/properties/hotkeys/properties/screenshot",
"$ref": "#/definitions/key",
"title": "Screenshot",
"default": "F8"
} }
} }
}, },