From 7cdd4013226d2cce4de386a6fdc7eba00680b3b0 Mon Sep 17 00:00:00 2001 From: emmaus Date: Fri, 5 Oct 2018 22:53:40 +0000 Subject: [PATCH] support loading and closing games --- Ryujinx.Graphics/Gal/IGalRenderer.cs | 2 + Ryujinx.Graphics/Gal/OpenGL/OGLRenderer.cs | 5 + Ryujinx.Graphics/NvGpu.cs | 13 +- Ryujinx.Graphics/NvGpuFifo.cs | 13 +- Ryujinx.HLE/Switch.cs | 2 + Ryujinx.UI/Emulation/EmulationController.cs | 206 +++++++++++++++++++ Ryujinx.UI/Program.cs | 8 +- Ryujinx.UI/Ryujinx.UI.csproj | 1 + Ryujinx.UI/UI/Images/closeGame.svg | 1 + Ryujinx.UI/UI/MainWindow.qml | 215 +++++++++++--------- Ryujinx/Ui/ConsoleLog.cs | 67 +++--- Ryujinx/Ui/GLScreen.cs | 15 +- 12 files changed, 413 insertions(+), 135 deletions(-) create mode 100644 Ryujinx.UI/Emulation/EmulationController.cs create mode 100644 Ryujinx.UI/UI/Images/closeGame.svg diff --git a/Ryujinx.Graphics/Gal/IGalRenderer.cs b/Ryujinx.Graphics/Gal/IGalRenderer.cs index 41e95a8789..d8cd99b7d1 100644 --- a/Ryujinx.Graphics/Gal/IGalRenderer.cs +++ b/Ryujinx.Graphics/Gal/IGalRenderer.cs @@ -8,6 +8,8 @@ namespace Ryujinx.Graphics.Gal void RunActions(); + void ClearActions(); + IGalConstBuffer Buffer { get; } IGalRenderTarget RenderTarget { get; } diff --git a/Ryujinx.Graphics/Gal/OpenGL/OGLRenderer.cs b/Ryujinx.Graphics/Gal/OpenGL/OGLRenderer.cs index a23541f3dd..4cde57557f 100644 --- a/Ryujinx.Graphics/Gal/OpenGL/OGLRenderer.cs +++ b/Ryujinx.Graphics/Gal/OpenGL/OGLRenderer.cs @@ -50,5 +50,10 @@ namespace Ryujinx.Graphics.Gal.OpenGL RenderAction(); } } + + public void ClearActions() + { + ActionsQueue.Clear(); + } } } \ No newline at end of file diff --git a/Ryujinx.Graphics/NvGpu.cs b/Ryujinx.Graphics/NvGpu.cs index 4c6abd234c..b8ac4bc86a 100644 --- a/Ryujinx.Graphics/NvGpu.cs +++ b/Ryujinx.Graphics/NvGpu.cs @@ -1,8 +1,9 @@ using Ryujinx.Graphics.Gal; +using System; namespace Ryujinx.Graphics { - public class NvGpu + public class NvGpu : IDisposable { public IGalRenderer Renderer { get; private set; } @@ -28,5 +29,15 @@ namespace Ryujinx.Graphics EngineM2mf = new NvGpuEngineM2mf(this); EngineP2mf = new NvGpuEngineP2mf(this); } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool Disposing) + { + Fifo.Dispose(); + } } } \ No newline at end of file diff --git a/Ryujinx.Graphics/NvGpuFifo.cs b/Ryujinx.Graphics/NvGpuFifo.cs index 16d16f5edf..e07b1f6e95 100644 --- a/Ryujinx.Graphics/NvGpuFifo.cs +++ b/Ryujinx.Graphics/NvGpuFifo.cs @@ -1,10 +1,11 @@ using Ryujinx.Graphics.Memory; using System.Collections.Concurrent; using System.Threading; +using System; namespace Ryujinx.Graphics { - public class NvGpuFifo + public class NvGpuFifo : IDisposable { private const int MacrosCount = 0x80; private const int MacroIndexMask = MacrosCount - 1; @@ -199,5 +200,15 @@ namespace Ryujinx.Graphics { Gpu.EngineM2mf.CallMethod(Vmm, PBEntry); } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool Disposing) + { + BufferQueue.Clear(); + } } } \ No newline at end of file diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs index a3f874ee59..e55affb31f 100644 --- a/Ryujinx.HLE/Switch.cs +++ b/Ryujinx.HLE/Switch.cs @@ -120,6 +120,8 @@ namespace Ryujinx.HLE System.Dispose(); VsyncEvent.Dispose(); + + Gpu.Dispose(); } } } diff --git a/Ryujinx.UI/Emulation/EmulationController.cs b/Ryujinx.UI/Emulation/EmulationController.cs new file mode 100644 index 0000000000..bfc7d4f4d1 --- /dev/null +++ b/Ryujinx.UI/Emulation/EmulationController.cs @@ -0,0 +1,206 @@ +using Qml.Net; +using Ryujinx.Audio; +using Ryujinx.Audio.OpenAL; +using Ryujinx.Graphics.Gal; +using Ryujinx.Graphics.Gal.OpenGL; +using Ryujinx.HLE; +using System; +using System.IO; +using System.Threading; + +namespace Ryujinx.UI.Emulation +{ + [Signal("failed", NetVariantType.String)] + [Signal("success")] + [Signal("loaded")] + [Signal("unloaded")] + class EmulationController + { + private static GLScreen RenderScreen; + private static Switch Device; + private static IAalOutput AudioOut; + private static IGalRenderer Renderer; + + private bool IsClosing; + + private bool isLoaded; + + public bool IsLoaded + { + get + { + return isLoaded; + } + + set + { + isLoaded = value; + + if (value) + { + this.ActivateSignal("loaded"); + } + else + { + this.ActivateSignal("unloaded"); + } + } + } + + public EmulationController() + { + RenderScreen = null; + Device = null; + AudioOut = null; + + IsLoaded = false; + } + + + public void LoadGameFile(string GameFile) + { + GameFile = GameFile.Replace("file:///", string.Empty); + + if (!File.Exists(GameFile)) + { + this.ActivateSignal("failed", $"File {GameFile} does not exist."); + + return; + } + + ShutdownEmulation(); + + InitializeEmulator(); + + switch (Path.GetExtension(GameFile).ToLowerInvariant()) + { + case ".xci": + Console.WriteLine("Loading as XCI."); + Device.LoadXci(GameFile); + break; + case ".nca": + Console.WriteLine("Loading as NCA."); + Device.LoadNca(GameFile); + break; + case ".nsp": + Console.WriteLine("Loading as NSP."); + Device.LoadNsp(GameFile); + break; + default: + Console.WriteLine("Loading as homebrew."); + Device.LoadProgram(GameFile); + break; + } + + StartRenderer(); + + IsLoaded = true; + + this.ActivateSignal("success"); + } + + public void LoadGameFolder(string ExeFsPath) + { + if (!Directory.Exists(ExeFsPath)) + { + this.ActivateSignal("failed", $"Directory {ExeFsPath} does not exist."); + + return; + } + + ShutdownEmulation(); + + InitializeEmulator(); + + string[] RomFsFiles = Directory.GetFiles(ExeFsPath, "*.istorage"); + + if (RomFsFiles.Length == 0) + { + RomFsFiles = Directory.GetFiles(ExeFsPath, "*.romfs"); + } + + if (RomFsFiles.Length > 0) + { + Console.WriteLine("Loading as cart with RomFS."); + + Device.LoadCart(ExeFsPath, RomFsFiles[0]); + } + else + { + Console.WriteLine("Loading as cart WITHOUT RomFS."); + + Device.LoadCart(ExeFsPath); + } + + StartRenderer(); + + IsLoaded = true; + + this.ActivateSignal("success"); + } + + public void ShutdownEmulation() + { + if(IsClosing || !IsLoaded) + { + return; + } + + IsClosing = true; + + Renderer?.ClearActions(); + + RenderScreen?.Close(); + + RenderScreen?.Dispose(); + + Device?.Dispose(); + + AudioOut?.Dispose(); + + ConsoleLog.Stop(); + + while (RenderScreen!=null && RenderScreen.Exists) + { + Thread.Sleep(5000); + } + + RenderScreen = null; + Device = null; + AudioOut = null; + Renderer = null; + + IsLoaded = false; + IsClosing = false; + } + + public void InitializeEmulator() + { + Renderer = new OGLRenderer(); + + AudioOut = new OpenALAudioOut(); + + Device = new Switch(Renderer, AudioOut); + + Config.Read(Device); + + ConsoleLog.Start(); + + Device.Log.Updated += ConsoleLog.Log; + } + + public void StartRenderer() + { + Thread RendererThread = new Thread(() => + { + using (RenderScreen = new GLScreen(Device, Renderer)) + { + RenderScreen.Closed += (o, e) => { ShutdownEmulation(); }; + RenderScreen.MainLoop(); + } + }); + + RendererThread.Start(); + } + } +} diff --git a/Ryujinx.UI/Program.cs b/Ryujinx.UI/Program.cs index 5cd76c3347..4925791652 100644 --- a/Ryujinx.UI/Program.cs +++ b/Ryujinx.UI/Program.cs @@ -1,5 +1,7 @@ using Qml.Net; -using System; +using Ryujinx.UI.Emulation; + +using static Qml.Net.Qml; namespace Ryujinx.UI { @@ -7,12 +9,14 @@ namespace Ryujinx.UI { static int Main(string[] args) { - QQuickStyle.SetStyle("Material"); + QQuickStyle.SetStyle("Fusion"); using (var Application = new QGuiApplication(args)) { using (var QmlEngine = new QQmlApplicationEngine()) { + RegisterType("Ryujinx"); + QmlEngine.Load("UI/MainWindow.qml"); return Application.Exec(); diff --git a/Ryujinx.UI/Ryujinx.UI.csproj b/Ryujinx.UI/Ryujinx.UI.csproj index 2e062ba657..cc9cc6bb26 100644 --- a/Ryujinx.UI/Ryujinx.UI.csproj +++ b/Ryujinx.UI/Ryujinx.UI.csproj @@ -6,6 +6,7 @@ + diff --git a/Ryujinx.UI/UI/Images/closeGame.svg b/Ryujinx.UI/UI/Images/closeGame.svg new file mode 100644 index 0000000000..40b5e39d6f --- /dev/null +++ b/Ryujinx.UI/UI/Images/closeGame.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Ryujinx.UI/UI/MainWindow.qml b/Ryujinx.UI/UI/MainWindow.qml index 424ea25214..a336a23716 100644 --- a/Ryujinx.UI/UI/MainWindow.qml +++ b/Ryujinx.UI/UI/MainWindow.qml @@ -1,8 +1,10 @@ -import QtQuick 2.9 +import QtQuick 2.11 import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.3 -import QtQuick.Controls.Material 2.1 -import QtQuick.Dialogs 1.0 +import QtQuick.Controls 2.4 +import QtQuick.Controls.Material 2.4 +import QtQuick.Dialogs 1.3 + +import Ryujinx 1.0 ApplicationWindow { id: window @@ -11,9 +13,39 @@ ApplicationWindow { visible: true title: "Ryujinx" - Material.theme: Material.Light - Material.accent: '#41cd52' - Material.primary: '#41cd52' + menuBar: MenuBar { + Menu{ + leftPadding: 5 + leftMargin: 0 + title: "&File" + + + MenuItem { + id: loadGameMenuItem + text: "Load Game File" + onClicked: { + loadDialog.loadGame() + } + } + + MenuItem { + id: loadGameFolderMenuItem + text: "Load Game Folder" + onClicked: { + loadDialog.loadGame() + } + } + + MenuSeparator{} + + MenuItem { + text: "Exit" + onClicked: { + Qt.quit() + } + } + } + } header: ToolBar { id: toolBar @@ -23,23 +55,6 @@ ApplicationWindow { anchors.fill: parent spacing: 20 - ToolButton { - id: drawerButton - text: qsTr("") - spacing: 3 - display: AbstractButton.IconOnly - icon.source: !drawer.visible ? "./Images/drawer.png" - : "./Images/arrowBack.svg" - - onClicked: { - if (drawer.visible) { - drawer.close() - } else { - drawer.open() - } - } - } - RowLayout { id: mainControlPanel spacing: 20 @@ -48,7 +63,8 @@ ApplicationWindow { ToolButton { id: openGameFileButton - display: AbstractButton.IconOnly + text: qsTr("Load Game File") + display: AbstractButton.TextUnderIcon icon.source: "./Images/loadGame.svg" ToolTip { text: qsTr("Load Game File") @@ -61,7 +77,8 @@ ApplicationWindow { ToolButton { id: openGameFolderButton - display: AbstractButton.IconOnly + text: qsTr("Load Game Folder") + display: AbstractButton.TextUnderIcon icon.source: "./Images/loadFolder.svg" ToolTip { text: qsTr("Load Game Folder") @@ -71,6 +88,24 @@ ApplicationWindow { loadDialog.loadGameFolder() } } + + ToolSeparator{} + + ToolButton { + id: closeGameButton + text: qsTr("Stop") + display: AbstractButton.TextUnderIcon + icon.source: "./Images/closeGame.svg" + enabled: false + + ToolTip { + text: qsTr("Close Current Game") + } + + onClicked: { + controller.shutdownEmulation() + } + } } } } @@ -80,77 +115,6 @@ ApplicationWindow { anchors.fill: parent } - Drawer { - id: drawer - width: window.width / 3 - height: window.height - topMargin: toolBar.height - spacing: 10 - - Rectangle{ - - Column{ - id: column - x: 40 - y: 20 - anchors.left: parent.left - anchors.leftMargin: 40 - anchors.top: parent.top - anchors.topMargin: 20 - spacing: 20 - - Image { - id: logo - width: 100 - height: 100 - fillMode: Image.PreserveAspectFit - source: "./Images/ryujinxLogo.png" - } - - Label { - id: appLabel - text: qsTr("Ryujinx") - font.bold: true - font.pointSize: 16 - font.weight: Font.Bold - lineHeight: 1.2 - } - - Rectangle{ - id: rectangle - anchors.top: appLabel.bottom - anchors.topMargin: 20 - - ListView { - id: drawerMenuList - width: 100 - height: 120 - anchors.top: parent.top - anchors.topMargin: 0 - currentIndex: -1 - - delegate: ItemDelegate { - width: parent.width - text: model.title - highlighted: ListView.isCurrentItem - } - - model: ListModel { - ListElement { title: "Games"} - ListElement { title: "Settings"} - ListElement { title: "Exit"} - } - - ScrollIndicator.vertical: ScrollIndicator { } - } - - } - - } - - } - } - FileDialog { id: loadDialog selectMultiple: false @@ -160,6 +124,17 @@ ApplicationWindow { "All Supported Formats (*.xci *.nca *.nsp *.nso *.nro)"] folder: shortcuts.home + onAccepted: { + if(selectFolder ) + { + controller.loadGameFolder(fileUrl) + } + else + { + controller.loadGameFile(fileUrl) + } + } + function loadGame() { selectFolder = false title = qsTr("Load Game File") @@ -174,9 +149,47 @@ ApplicationWindow { open() } } -} -/*##^## Designer { - D{i:272;anchors_height:120;anchors_width:100} + + + EmulationController { + id: controller + + onFailed: function(result) { + // alertBox.title = "Failed to load game" + // alertBox.text = result + + // alertBox.open() + + loadGameMenuItem.enabled = true + loadGameFolderMenuItem.enabled = true + openGameFileButton.enabled = true + openGameFolderButton.enabled = true + closeGameButton.enabled = false + } + + onSuccess: { + loadGameMenuItem.enabled = false + loadGameFolderMenuItem.enabled = false + openGameFileButton.enabled = false + openGameFolderButton.enabled = false + closeGameButton.enabled = true + } + + onLoaded: { + loadGameMenuItem.enabled = false + loadGameFolderMenuItem.enabled = false + openGameFileButton.enabled = false + openGameFolderButton.enabled = false + closeGameButton.enabled = true + } + + onUnloaded: { + loadGameMenuItem.enabled = true + loadGameFolderMenuItem.enabled = true + openGameFileButton.enabled = true + openGameFolderButton.enabled = true + closeGameButton.enabled = false + } + } } - ##^##*/ diff --git a/Ryujinx/Ui/ConsoleLog.cs b/Ryujinx/Ui/ConsoleLog.cs index 6fb92e333f..e7654c3b5a 100644 --- a/Ryujinx/Ui/ConsoleLog.cs +++ b/Ryujinx/Ui/ConsoleLog.cs @@ -6,7 +6,7 @@ using System.Threading; namespace Ryujinx { - static class ConsoleLog + public static class ConsoleLog { private static Thread MessageThread; @@ -26,31 +26,7 @@ namespace Ryujinx { LogLevel.Error, ConsoleColor.Red } }; - MessageQueue = new BlockingCollection(); - - ConsoleLock = new object(); - - MessageThread = new Thread(() => - { - while (!MessageQueue.IsCompleted) - { - try - { - PrintLog(MessageQueue.Take()); - } - catch (InvalidOperationException) - { - // IOE means that Take() was called on a completed collection. - // Some other thread can call CompleteAdding after we pass the - // IsCompleted check but before we call Take. - // We can simply catch the exception since the loop will break - // on the next iteration. - } - } - }); - - MessageThread.IsBackground = true; - MessageThread.Start(); + Start(); } private static void PrintLog(LogEventArgs e) @@ -84,5 +60,44 @@ namespace Ryujinx MessageQueue.Add(e); } } + + public static void Stop() + { + MessageQueue?.CompleteAdding(); + } + + public static void Start() + { + if (MessageQueue != null && !MessageQueue.IsCompleted) + { + return; + } + + MessageQueue = new BlockingCollection(); + + ConsoleLock = new object(); + + MessageThread = new Thread(() => + { + while (!MessageQueue.IsCompleted) + { + try + { + PrintLog(MessageQueue.Take()); + } + catch (InvalidOperationException) + { + // IOE means that Take() was called on a completed collection. + // Some other thread can call CompleteAdding after we pass the + // IsCompleted check but before we call Take. + // We can simply catch the exception since the loop will break + // on the next iteration. + } + } + }); + + MessageThread.IsBackground = true; + MessageThread.Start(); + } } } \ No newline at end of file diff --git a/Ryujinx/Ui/GLScreen.cs b/Ryujinx/Ui/GLScreen.cs index 27f3f08b56..4948fa0d91 100644 --- a/Ryujinx/Ui/GLScreen.cs +++ b/Ryujinx/Ui/GLScreen.cs @@ -60,7 +60,7 @@ namespace Ryujinx long Ticks = 0; - while (Exists && !IsExiting) + while (!IsDisposed && Exists && !IsExiting) { if (Device.WaitFifo()) { @@ -109,7 +109,7 @@ namespace Ryujinx { ProcessEvents(); - if (!IsExiting) + if (!IsDisposed && !IsExiting) { UpdateFrame(); @@ -266,11 +266,18 @@ namespace Ryujinx TitleEvent = true; - SwapBuffers(); + if (!IsDisposed && Exists) + { + SwapBuffers(); + } + else + { + + } Device.System.SignalVsync(); - Device.VsyncEvent.Set(); + Device.VsyncEvent?.Set(); } protected override void OnUnload(EventArgs e)