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 Key ToggleVsync { get; set; }
public Key Screenshot { get; set; }
}
}

View file

@ -6,6 +6,8 @@ namespace Ryujinx.Graphics.GAL
{
public interface IRenderer : IDisposable
{
event EventHandler<ScreenCaptureImageInfo> 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();
}
}

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;
public event EventHandler<ScreenCaptureImageInfo> 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);
}
}
}

View file

@ -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();

View file

@ -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": [],

View file

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

View file

@ -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<InputConfig>
{
@ -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;

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.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;
@ -1378,6 +1383,7 @@ namespace Ryujinx.Ui
private void ActionMenu_StateChanged(object o, StateChangedArgs args)
{
_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)

View file

@ -1,14 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkApplicationWindow" id="_mainWin">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Ryujinx</property>
<property name="window_position">center</property>
<child type="titlebar">
<placeholder/>
</child>
<child>
<object class="GtkBox" id="_box">
<property name="visible">True</property>
@ -332,6 +329,15 @@
<signal name="activate" handler="Scan_Amiibo" swapped="no"/>
</object>
</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>
</child>
</object>
@ -450,7 +456,7 @@
<property name="can_focus">True</property>
<property name="reorderable">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">
<object class="GtkTreeSelection" id="_gameTableSelection"/>
</child>
@ -484,7 +490,7 @@
<property name="visible">True</property>
<property name="can_focus">False</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>
<object class="GtkImage">
<property name="name">RefreshList</property>
@ -547,8 +553,7 @@
<object class="GtkEventBox">
<property name="visible">True</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>
<object class="GtkLabel" id="_vSyncStatus">
<property name="visible">True</property>
@ -581,8 +586,7 @@
<object class="GtkEventBox">
<property name="visible">True</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>
<object class="GtkLabel" id="_dockedMode">
<property name="visible">True</property>
@ -614,8 +618,7 @@
<object class="GtkEventBox">
<property name="visible">True</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>
<object class="GtkLabel" id="_aspectRatio">
<property name="visible">True</property>
@ -713,35 +716,6 @@
<property name="fill">True</property>
<property name="position">1</property>
</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>
<object class="GtkBox">
@ -783,6 +757,33 @@
<property name="position">4</property>
</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>
</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>
<packing>
<property name="expand">False</property>

View file

@ -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<StatusUpdatedEventArgs> 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<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()
{
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()
@ -528,6 +592,11 @@ namespace Ryujinx.Ui
state |= KeyboardHotkeyState.ToggleVSync;
}
if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
{
state |= KeyboardHotkeyState.Screenshot;
}
return state;
}
}

View file

@ -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"
}
}
},