Add Screenshot Feature (#2354)
* Add internal screenshot capabilities * update version notice
This commit is contained in:
parent
a79b39b913
commit
28618c58d7
12 changed files with 220 additions and 56 deletions
|
@ -3,5 +3,6 @@
|
|||
public struct KeyboardHotkeys
|
||||
{
|
||||
public Key ToggleVsync { get; set; }
|
||||
public Key Screenshot { get; set; }
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
22
Ryujinx.Graphics.GAL/ScreenCaptureImageInfo.cs
Normal file
22
Ryujinx.Graphics.GAL/ScreenCaptureImageInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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": [],
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Reference in a new issue