aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs1
-rw-r--r--Ryujinx.Graphics.GAL/IRenderer.cs4
-rw-r--r--Ryujinx.Graphics.GAL/ScreenCaptureImageInfo.cs22
-rw-r--r--Ryujinx.Graphics.OpenGL/Renderer.cs12
-rw-r--r--Ryujinx.Graphics.OpenGL/Window.cs19
-rw-r--r--Ryujinx/Config.json5
-rw-r--r--Ryujinx/Configuration/ConfigurationFileFormat.cs2
-rw-r--r--Ryujinx/Configuration/ConfigurationState.cs16
-rw-r--r--Ryujinx/Ui/MainWindow.cs30
-rw-r--r--Ryujinx/Ui/MainWindow.glade83
-rw-r--r--Ryujinx/Ui/RendererWidgetBase.cs73
-rw-r--r--Ryujinx/_schema.json9
12 files changed, 220 insertions, 56 deletions
diff --git a/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs
index 8e9f168d..47772ea8 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 d03cb4c0..18f10915 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<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();
}
}
diff --git a/Ryujinx.Graphics.GAL/ScreenCaptureImageInfo.cs b/Ryujinx.Graphics.GAL/ScreenCaptureImageInfo.cs
new file mode 100644
index 00000000..227d64b6
--- /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 a2be4373..001cac8d 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<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);
+ }
}
}
diff --git a/Ryujinx.Graphics.OpenGL/Window.cs b/Ryujinx.Graphics.OpenGL/Window.cs
index b7525ae5..35b04d6d 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 033186fe..3d4ea23b 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 04a51815..4634dafe 100644
--- a/Ryujinx/Configuration/ConfigurationFileFormat.cs
+++ b/Ryujinx/Configuration/ConfigurationFileFormat.cs
@@ -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; }
diff --git a/Ryujinx/Configuration/ConfigurationState.cs b/Ryujinx/Configuration/ConfigurationState.cs
index 1769dfa9..c98fbcca 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<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;
diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs
index 113ac639..eb1e10b3 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 e974d878..7bf38f47 100644
--- a/Ryujinx/Ui/MainWindow.glade
+++ b/Ryujinx/Ui/MainWindow.glade
@@ -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>
@@ -714,35 +717,6 @@
<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">
<property name="visible">True</property>
@@ -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>
diff --git a/Ryujinx/Ui/RendererWidgetBase.cs b/Ryujinx/Ui/RendererWidgetBase.cs
index 699f06c5..dee5cbb6 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<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;
}
diff --git a/Ryujinx/_schema.json b/Ryujinx/_schema.json
index f819b9d6..47da28b1 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"
}
}
},