Compare commits

...

5 Commits

Author SHA1 Message Date
riperiperi
65778a6b78 GPU: Don't trigger uploads for redundant buffer updates (#3828)
* Initial implementation

* Actually do The Thing

* Add remark about performance to IVirtualMemoryManager
2022-11-24 15:50:15 +01:00
Mary-nyan
f4e879a1e6 Reduce usage of Marshal.PtrToStructure and Marshal.StructureToPtr (#3805)
* common: Make BinaryReaderExtensions Read & Write take unamanged types

This allows us to not rely on Marshal.PtrToStructure and Marshal.StructureToPtr for those.

* common: Make MemoryHelper Read & Write takes unamanged types

* Update Marshal.SizeOf => Unsafe.SizeOf when appropriate and start moving software applet to unmanaged types
2022-11-24 15:26:29 +01:00
Ac_K
a1ddaa2736 ui: Fixes disposing on GTK/Avalonia and Firmware Messages on Avalonia (#3885)
* ui: Only wait on _exitEvent when MainLoop is active under GTK

This fixes a dispose issue under Horizon/GTK, we don't check if the ApplicationClient is null so it throw NCE. We don't check if the main loop is active and waiting an event which is set in the main loop... So that could lead to a freeze.

Everything works fine in GTK now.

Related issue: https://github.com/Ryujinx/Ryujinx/issues/3873

As a side note, same kind of issue appear in Avalonia UI too. Firmware's popup doesn't show anything and the emulator just freeze.

* TSRBerry's change

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

* Fix Avalonia crashing/freezing

* Add Avalonia OpenGL fixes

* Fix firmware popup on windows

* Fixes everything

* Add _initialized bool to VulkanRenderer and OpenGL Window

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
2022-11-24 15:08:27 +01:00
Mary-nyan
008286b79f Ryujinx.Ava: Add missing redefinition of app name (#3890)
Before this, Ryujinx would possibly report as "Avalonia Application".
2022-11-24 14:52:39 +01:00
gdkchan
a0c77f8d11 Fix NRE on Avalonia for error applets with unknown error message (#3888) 2022-11-24 09:31:00 +01:00
36 changed files with 295 additions and 207 deletions

View File

@@ -21,6 +21,8 @@ namespace Ryujinx.Ava
{ {
public override void Initialize() public override void Initialize()
{ {
Name = $"Ryujinx {Program.Version}";
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }

View File

@@ -349,7 +349,10 @@ namespace Ryujinx.Ava
_isActive = false; _isActive = false;
if (_renderingThread.IsAlive)
{
_renderingThread.Join(); _renderingThread.Join();
}
DisplaySleep.Restore(); DisplaySleep.Restore();
@@ -417,7 +420,6 @@ namespace Ryujinx.Ava
public async Task<bool> LoadGuestApplication() public async Task<bool> LoadGuestApplication()
{ {
InitializeSwitchInstance(); InitializeSwitchInstance();
MainWindow.UpdateGraphicsConfig(); MainWindow.UpdateGraphicsConfig();
SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
@@ -428,17 +430,16 @@ namespace Ryujinx.Ava
{ {
if (userError == UserError.NoFirmware) if (userError == UserError.NoFirmware)
{ {
string message = string.Format(LocaleManager.Instance["DialogFirmwareInstallEmbeddedMessage"],
firmwareVersion.VersionString);
UserResult result = await ContentDialogHelper.CreateConfirmationDialog( UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance["DialogFirmwareNoFirmwareInstalledMessage"], message, LocaleManager.Instance["DialogFirmwareNoFirmwareInstalledMessage"],
LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], ""); string.Format(LocaleManager.Instance["DialogFirmwareInstallEmbeddedMessage"], firmwareVersion.VersionString),
LocaleManager.Instance["InputDialogYes"],
LocaleManager.Instance["InputDialogNo"],
"");
if (result != UserResult.Yes) if (result != UserResult.Yes)
{ {
Dispatcher.UIThread.Post(async () => await await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
UserErrorDialog.ShowUserErrorDialog(userError, _parent));
Device.Dispose(); Device.Dispose();
return false; return false;
@@ -447,8 +448,7 @@ namespace Ryujinx.Ava
if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _)) if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _))
{ {
Dispatcher.UIThread.Post(async () => await await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
UserErrorDialog.ShowUserErrorDialog(userError, _parent));
Device.Dispose(); Device.Dispose();
return false; return false;
@@ -461,11 +461,9 @@ namespace Ryujinx.Ava
_parent.RefreshFirmwareStatus(); _parent.RefreshFirmwareStatus();
string message = string.Format(LocaleManager.Instance["DialogFirmwareInstallEmbeddedSuccessMessage"], firmwareVersion.VersionString);
await ContentDialogHelper.CreateInfoDialog( await ContentDialogHelper.CreateInfoDialog(
string.Format(LocaleManager.Instance["DialogFirmwareInstalledMessage"], firmwareVersion.VersionString), string.Format(LocaleManager.Instance["DialogFirmwareInstalledMessage"], firmwareVersion.VersionString),
message, string.Format(LocaleManager.Instance["DialogFirmwareInstallEmbeddedSuccessMessage"], firmwareVersion.VersionString),
LocaleManager.Instance["InputDialogOk"], LocaleManager.Instance["InputDialogOk"],
"", "",
LocaleManager.Instance["RyujinxInfo"]); LocaleManager.Instance["RyujinxInfo"]);
@@ -473,9 +471,7 @@ namespace Ryujinx.Ava
} }
else else
{ {
Dispatcher.UIThread.Post(async () => await await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
UserErrorDialog.ShowUserErrorDialog(userError, _parent));
Device.Dispose(); Device.Dispose();
return false; return false;
@@ -514,7 +510,7 @@ namespace Ryujinx.Ava
} }
else if (File.Exists(ApplicationPath)) else if (File.Exists(ApplicationPath))
{ {
switch (System.IO.Path.GetExtension(ApplicationPath).ToLowerInvariant()) switch (Path.GetExtension(ApplicationPath).ToLowerInvariant())
{ {
case ".xci": case ".xci":
{ {

View File

@@ -168,7 +168,7 @@ namespace Ryujinx.Ava.Ui.Applet
object response = await msgDialog.Run(); object response = await msgDialog.Run();
if (response != null && buttons.Length > 1 && (int)response != buttons.Length - 1) if (response != null && buttons != null && buttons.Length > 1 && (int)response != buttons.Length - 1)
{ {
showDetails = true; showDetails = true;
} }

View File

@@ -69,12 +69,12 @@ namespace Ryujinx.Ava.Ui.Controls
public void MakeCurrent() public void MakeCurrent()
{ {
Context.MakeCurrent(_window); Context?.MakeCurrent(_window);
} }
public void MakeCurrent(NativeWindowBase window) public void MakeCurrent(NativeWindowBase window)
{ {
Context.MakeCurrent(window); Context?.MakeCurrent(window);
} }
public void SwapBuffers() public void SwapBuffers()

View File

@@ -251,9 +251,12 @@ namespace Ryujinx.Ava.Ui.Windows
AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this); AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
if (!AppHost.LoadGuestApplication().Result) Dispatcher.UIThread.Post(async () =>
{
if (!await AppHost.LoadGuestApplication())
{ {
AppHost.DisposeContext(); AppHost.DisposeContext();
AppHost = null;
return; return;
} }
@@ -264,11 +267,13 @@ namespace Ryujinx.Ava.Ui.Windows
SwitchToGameControl(startFullscreen); SwitchToGameControl(startFullscreen);
_currentEmulatedGamePath = path; _currentEmulatedGamePath = path;
Thread gameThread = new Thread(InitializeGame)
Thread gameThread = new(InitializeGame)
{ {
Name = "GUI.WindowThread" Name = "GUI.WindowThread"
}; };
gameThread.Start(); gameThread.Start();
});
} }
private void InitializeGame() private void InitializeGame()
@@ -546,10 +551,12 @@ namespace Ryujinx.Ava.Ui.Windows
{ {
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
{ {
DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed); if (DateTime.TryParse(appMetadata.LastPlayed, out DateTime lastPlayedDateTime))
{
double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds; double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
}
}); });
} }

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.Common namespace Ryujinx.Common
@@ -7,49 +8,15 @@ namespace Ryujinx.Common
public static class BinaryReaderExtensions public static class BinaryReaderExtensions
{ {
public unsafe static T ReadStruct<T>(this BinaryReader reader) public unsafe static T ReadStruct<T>(this BinaryReader reader)
where T : struct where T : unmanaged
{ {
int size = Marshal.SizeOf<T>(); return MemoryMarshal.Cast<byte, T>(reader.ReadBytes(Unsafe.SizeOf<T>()))[0];
byte[] data = reader.ReadBytes(size);
fixed (byte* ptr = data)
{
return Marshal.PtrToStructure<T>((IntPtr)ptr);
}
}
public unsafe static T[] ReadStructArray<T>(this BinaryReader reader, int count)
where T : struct
{
int size = Marshal.SizeOf<T>();
T[] result = new T[count];
for (int i = 0; i < count; i++)
{
byte[] data = reader.ReadBytes(size);
fixed (byte* ptr = data)
{
result[i] = Marshal.PtrToStructure<T>((IntPtr)ptr);
}
}
return result;
} }
public unsafe static void WriteStruct<T>(this BinaryWriter writer, T value) public unsafe static void WriteStruct<T>(this BinaryWriter writer, T value)
where T : struct where T : unmanaged
{ {
long size = Marshal.SizeOf<T>(); ReadOnlySpan<byte> data = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref value, 1));
byte[] data = new byte[size];
fixed (byte* ptr = data)
{
Marshal.StructureToPtr<T>(value, (IntPtr)ptr, false);
}
writer.Write(data); writer.Write(data);
} }

View File

@@ -180,6 +180,37 @@ namespace Ryujinx.Cpu.Jit
WriteImpl(va, data); WriteImpl(va, data);
} }
/// <inheritdoc/>
public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
{
if (data.Length == 0)
{
return false;
}
SignalMemoryTracking(va, (ulong)data.Length, false);
if (IsContiguousAndMapped(va, data.Length))
{
var target = _backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length);
bool changed = !data.SequenceEqual(target);
if (changed)
{
data.CopyTo(target);
}
return changed;
}
else
{
WriteImpl(va, data);
return true;
}
}
/// <summary> /// <summary>
/// Writes data to CPU mapped memory. /// Writes data to CPU mapped memory.
/// </summary> /// </summary>

View File

@@ -307,6 +307,34 @@ namespace Ryujinx.Cpu.Jit
} }
} }
/// <inheritdoc/>
public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
{
try
{
SignalMemoryTracking(va, (ulong)data.Length, false);
Span<byte> target = _addressSpaceMirror.GetSpan(va, data.Length);
bool changed = !data.SequenceEqual(target);
if (changed)
{
data.CopyTo(target);
}
return changed;
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
return true;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false) public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
{ {

View File

@@ -1,6 +1,7 @@
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -23,34 +24,18 @@ namespace Ryujinx.Cpu
} }
} }
public unsafe static T Read<T>(IVirtualMemoryManager memory, ulong position) where T : struct public unsafe static T Read<T>(IVirtualMemoryManager memory, ulong position) where T : unmanaged
{ {
long size = Marshal.SizeOf<T>(); return MemoryMarshal.Cast<byte, T>(memory.GetSpan(position, Unsafe.SizeOf<T>()))[0];
byte[] data = new byte[size];
memory.Read(position, data);
fixed (byte* ptr = data)
{
return Marshal.PtrToStructure<T>((IntPtr)ptr);
}
} }
public unsafe static ulong Write<T>(IVirtualMemoryManager memory, ulong position, T value) where T : struct public unsafe static ulong Write<T>(IVirtualMemoryManager memory, ulong position, T value) where T : unmanaged
{ {
long size = Marshal.SizeOf<T>(); ReadOnlySpan<byte> data = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref value, 1));
byte[] data = new byte[size];
fixed (byte* ptr = data)
{
Marshal.StructureToPtr<T>(value, (IntPtr)ptr, false);
}
memory.Write(position, data); memory.Write(position, data);
return (ulong)size; return (ulong)data.Length;
} }
public static string ReadAsciiString(IVirtualMemoryManager memory, ulong position, long maxSize = -1) public static string ReadAsciiString(IVirtualMemoryManager memory, ulong position, long maxSize = -1)

View File

@@ -422,7 +422,11 @@ namespace Ryujinx.Graphics.GAL.Multithreading
// Stop the GPU thread. // Stop the GPU thread.
_disposed = true; _disposed = true;
if (_gpuThread != null && _gpuThread.IsAlive)
{
_gpuThread.Join(); _gpuThread.Join();
}
// Dispose the renderer. // Dispose the renderer.
_baseRenderer.Dispose(); _baseRenderer.Dispose();

View File

@@ -8,6 +8,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary> /// </summary>
class ConstantBufferUpdater class ConstantBufferUpdater
{ {
private const int UniformDataCacheSize = 512;
private readonly GpuChannel _channel; private readonly GpuChannel _channel;
private readonly DeviceStateWithShadow<ThreedClassState> _state; private readonly DeviceStateWithShadow<ThreedClassState> _state;
@@ -16,6 +18,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private ulong _ubBeginCpuAddress = 0; private ulong _ubBeginCpuAddress = 0;
private ulong _ubFollowUpAddress = 0; private ulong _ubFollowUpAddress = 0;
private ulong _ubByteCount = 0; private ulong _ubByteCount = 0;
private int _ubIndex = 0;
private int[] _ubData = new int[UniformDataCacheSize];
/// <summary> /// <summary>
/// Creates a new instance of the constant buffer updater. /// Creates a new instance of the constant buffer updater.
@@ -108,9 +112,16 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (_ubFollowUpAddress != 0) if (_ubFollowUpAddress != 0)
{ {
var memoryManager = _channel.MemoryManager; var memoryManager = _channel.MemoryManager;
Span<byte> data = MemoryMarshal.Cast<int, byte>(_ubData.AsSpan(0, (int)(_ubByteCount / 4)));
if (memoryManager.Physical.WriteWithRedundancyCheck(_ubBeginCpuAddress, data))
{
memoryManager.Physical.BufferCache.ForceDirty(memoryManager, _ubFollowUpAddress - _ubByteCount, _ubByteCount); memoryManager.Physical.BufferCache.ForceDirty(memoryManager, _ubFollowUpAddress - _ubByteCount, _ubByteCount);
}
_ubFollowUpAddress = 0; _ubFollowUpAddress = 0;
_ubIndex = 0;
} }
} }
@@ -124,7 +135,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ulong address = uniformBuffer.Address.Pack() + (uint)uniformBuffer.Offset; ulong address = uniformBuffer.Address.Pack() + (uint)uniformBuffer.Offset;
if (_ubFollowUpAddress != address) if (_ubFollowUpAddress != address || _ubIndex == _ubData.Length)
{ {
FlushUboDirty(); FlushUboDirty();
@@ -132,8 +143,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_ubBeginCpuAddress = _channel.MemoryManager.Translate(address); _ubBeginCpuAddress = _channel.MemoryManager.Translate(address);
} }
var byteData = MemoryMarshal.Cast<int, byte>(MemoryMarshal.CreateSpan(ref argument, 1)); _ubData[_ubIndex++] = argument;
_channel.MemoryManager.Physical.WriteUntracked(_ubBeginCpuAddress + _ubByteCount, byteData);
_ubFollowUpAddress = address + 4; _ubFollowUpAddress = address + 4;
_ubByteCount += 4; _ubByteCount += 4;
@@ -153,7 +163,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ulong size = (ulong)data.Length * 4; ulong size = (ulong)data.Length * 4;
if (_ubFollowUpAddress != address) if (_ubFollowUpAddress != address || _ubIndex + data.Length > _ubData.Length)
{ {
FlushUboDirty(); FlushUboDirty();
@@ -161,8 +171,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_ubBeginCpuAddress = _channel.MemoryManager.Translate(address); _ubBeginCpuAddress = _channel.MemoryManager.Translate(address);
} }
var byteData = MemoryMarshal.Cast<int, byte>(data); data.CopyTo(_ubData.AsSpan(_ubIndex));
_channel.MemoryManager.Physical.WriteUntracked(_ubBeginCpuAddress + _ubByteCount, byteData); _ubIndex += data.Length;
_ubFollowUpAddress = address + size; _ubFollowUpAddress = address + size;
_ubByteCount += size; _ubByteCount += size;

View File

@@ -242,6 +242,19 @@ namespace Ryujinx.Graphics.Gpu.Memory
WriteImpl(range, data, _cpuMemory.WriteUntracked); WriteImpl(range, data, _cpuMemory.WriteUntracked);
} }
/// <summary>
/// Writes data to the application process, returning false if the data was not changed.
/// This triggers read memory tracking, as a redundancy check would be useless if the data is not up to date.
/// </summary>
/// <remarks>The memory manager can return that memory has changed when it hasn't to avoid expensive data copies.</remarks>
/// <param name="address">Address to write into</param>
/// <param name="data">Data to be written</param>
/// <returns>True if the data was changed, false otherwise</returns>
public bool WriteWithRedundancyCheck(ulong address, ReadOnlySpan<byte> data)
{
return _cpuMemory.WriteWithRedundancyCheck(address, data);
}
private delegate void WriteCallback(ulong address, ReadOnlySpan<byte> data); private delegate void WriteCallback(ulong address, ReadOnlySpan<byte> data);
/// <summary> /// <summary>

View File

@@ -10,6 +10,8 @@ namespace Ryujinx.Graphics.OpenGL
private const int TextureCount = 3; private const int TextureCount = 3;
private readonly OpenGLRenderer _renderer; private readonly OpenGLRenderer _renderer;
private bool _initialized;
private int _width; private int _width;
private int _height; private int _height;
private int _copyFramebufferHandle; private int _copyFramebufferHandle;
@@ -179,6 +181,7 @@ namespace Ryujinx.Graphics.OpenGL
public void InitializeBackgroundContext(IOpenGLContext baseContext) public void InitializeBackgroundContext(IOpenGLContext baseContext)
{ {
BackgroundContext = new BackgroundContextWorker(baseContext); BackgroundContext = new BackgroundContextWorker(baseContext);
_initialized = true;
} }
public void CaptureFrame(int x, int y, int width, int height, bool isBgra, bool flipX, bool flipY) public void CaptureFrame(int x, int y, int width, int height, bool isBgra, bool flipX, bool flipY)
@@ -193,6 +196,11 @@ namespace Ryujinx.Graphics.OpenGL
public void Dispose() public void Dispose()
{ {
if (!_initialized)
{
return;
}
BackgroundContext.Dispose(); BackgroundContext.Dispose();
if (_copyFramebufferHandle != 0) if (_copyFramebufferHandle != 0)

View File

@@ -22,6 +22,8 @@ namespace Ryujinx.Graphics.Vulkan
private Device _device; private Device _device;
private WindowBase _window; private WindowBase _window;
private bool _initialized;
internal FormatCapabilities FormatCapabilities { get; private set; } internal FormatCapabilities FormatCapabilities { get; private set; }
internal HardwareCapabilities Capabilities; internal HardwareCapabilities Capabilities;
@@ -266,6 +268,8 @@ namespace Ryujinx.Graphics.Vulkan
LoadFeatures(supportedExtensions, maxQueueCount, queueFamilyIndex); LoadFeatures(supportedExtensions, maxQueueCount, queueFamilyIndex);
_window = new Window(this, _surface, _physicalDevice, _device); _window = new Window(this, _surface, _physicalDevice, _device);
_initialized = true;
} }
public BufferHandle CreateBuffer(int size) public BufferHandle CreateBuffer(int size)
@@ -573,6 +577,11 @@ namespace Ryujinx.Graphics.Vulkan
public unsafe void Dispose() public unsafe void Dispose()
{ {
if (!_initialized)
{
return;
}
CommandBufferPool.Dispose(); CommandBufferPool.Dispose();
BackgroundResources.Dispose(); BackgroundResources.Dispose();
_counters.Dispose(); _counters.Dispose();

View File

@@ -1,4 +1,5 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
using Ryujinx.HLE.HOS.Services.Am.AppletAE; using Ryujinx.HLE.HOS.Services.Am.AppletAE;
@@ -9,6 +10,7 @@ using Ryujinx.Memory;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -78,13 +80,13 @@ namespace Ryujinx.HLE.HOS.Applets
var launchParams = _normalSession.Pop(); var launchParams = _normalSession.Pop();
var keyboardConfig = _normalSession.Pop(); var keyboardConfig = _normalSession.Pop();
_isBackground = keyboardConfig.Length == Marshal.SizeOf<SoftwareKeyboardInitialize>(); _isBackground = keyboardConfig.Length == Unsafe.SizeOf<SoftwareKeyboardInitialize>();
if (_isBackground) if (_isBackground)
{ {
// Initialize the keyboard applet in background mode. // Initialize the keyboard applet in background mode.
_keyboardBackgroundInitialize = ReadStruct<SoftwareKeyboardInitialize>(keyboardConfig); _keyboardBackgroundInitialize = MemoryMarshal.Read<SoftwareKeyboardInitialize>(keyboardConfig);
_backgroundState = InlineKeyboardState.Uninitialized; _backgroundState = InlineKeyboardState.Uninitialized;
if (_device.UiHandler == null) if (_device.UiHandler == null)
@@ -342,7 +344,7 @@ namespace Ryujinx.HLE.HOS.Applets
else else
{ {
int wordsCount = reader.ReadInt32(); int wordsCount = reader.ReadInt32();
int wordSize = Marshal.SizeOf<SoftwareKeyboardUserWord>(); int wordSize = Unsafe.SizeOf<SoftwareKeyboardUserWord>();
remaining = stream.Length - stream.Position; remaining = stream.Length - stream.Position;
if (wordsCount > MaxUserWords) if (wordsCount > MaxUserWords)
@@ -359,8 +361,7 @@ namespace Ryujinx.HLE.HOS.Applets
for (int word = 0; word < wordsCount; word++) for (int word = 0; word < wordsCount; word++)
{ {
byte[] wordData = reader.ReadBytes(wordSize); _keyboardBackgroundUserWords[word] = reader.ReadStruct<SoftwareKeyboardUserWord>();
_keyboardBackgroundUserWords[word] = ReadStruct<SoftwareKeyboardUserWord>(wordData);
} }
} }
} }
@@ -369,27 +370,25 @@ namespace Ryujinx.HLE.HOS.Applets
case InlineKeyboardRequest.SetCustomizeDic: case InlineKeyboardRequest.SetCustomizeDic:
// Read the custom dic data. // Read the custom dic data.
remaining = stream.Length - stream.Position; remaining = stream.Length - stream.Position;
if (remaining != Marshal.SizeOf<SoftwareKeyboardCustomizeDic>()) if (remaining != Unsafe.SizeOf<SoftwareKeyboardCustomizeDic>())
{ {
Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes"); Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes");
} }
else else
{ {
var keyboardDicData = reader.ReadBytes((int)remaining); _keyboardBackgroundDic = reader.ReadStruct<SoftwareKeyboardCustomizeDic>();
_keyboardBackgroundDic = ReadStruct<SoftwareKeyboardCustomizeDic>(keyboardDicData);
} }
break; break;
case InlineKeyboardRequest.SetCustomizedDictionaries: case InlineKeyboardRequest.SetCustomizedDictionaries:
// Read the custom dictionaries data. // Read the custom dictionaries data.
remaining = stream.Length - stream.Position; remaining = stream.Length - stream.Position;
if (remaining != Marshal.SizeOf<SoftwareKeyboardDictSet>()) if (remaining != Unsafe.SizeOf<SoftwareKeyboardDictSet>())
{ {
Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes"); Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes");
} }
else else
{ {
var keyboardDictData = reader.ReadBytes((int)remaining); _keyboardBackgroundDictSet = reader.ReadStruct<SoftwareKeyboardDictSet>();
_keyboardBackgroundDictSet = ReadStruct<SoftwareKeyboardDictSet>(keyboardDictData);
} }
break; break;
case InlineKeyboardRequest.Calc: case InlineKeyboardRequest.Calc:

View File

@@ -5,10 +5,9 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
/// <summary> /// <summary>
/// A structure used by SetCustomizeDic request to software keyboard. /// A structure used by SetCustomizeDic request to software keyboard.
/// </summary> /// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 4)] [StructLayout(LayoutKind.Sequential, Size = 0x70)]
struct SoftwareKeyboardCustomizeDic struct SoftwareKeyboardCustomizeDic
{ {
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 112)] // Unknown
public byte[] Unknown;
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Runtime.InteropServices; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{ {
@@ -21,8 +22,7 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
/// <summary> /// <summary>
/// Array of word entries in the buffer. /// Array of word entries in the buffer.
/// </summary> /// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)] public Array24<ulong> Entries;
public ulong[] Entries;
/// <summary> /// <summary>
/// Number of used entries in the Entries field. /// Number of used entries in the Entries field.

View File

@@ -5,10 +5,9 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
/// <summary> /// <summary>
/// A structure used by SetUserWordInfo request to the software keyboard. /// A structure used by SetUserWordInfo request to the software keyboard.
/// </summary> /// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 4)] [StructLayout(LayoutKind.Sequential, Size = 0x64)]
struct SoftwareKeyboardUserWord struct SoftwareKeyboardUserWord
{ {
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 100)] // Unknown
public byte[] Unknown;
} }
} }

View File

@@ -479,7 +479,10 @@ namespace Ryujinx.HLE.HOS
AudioRendererManager.Dispose(); AudioRendererManager.Dispose();
if (LibHacHorizonManager.ApplicationClient != null)
{
LibHacHorizonManager.PmClient.Fs.UnregisterProgram(LibHacHorizonManager.ApplicationClient.Os.GetCurrentProcessId().Value).ThrowIfFailure(); LibHacHorizonManager.PmClient.Fs.UnregisterProgram(LibHacHorizonManager.ApplicationClient.Os.GetCurrentProcessId().Value).ThrowIfFailure();
}
KernelContext.Dispose(); KernelContext.Dispose();
} }

View File

@@ -1,9 +1,12 @@
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService
{ {
[StructLayout(LayoutKind.Sequential, Pack = 0x8, CharSet = CharSet.Ansi)] [StructLayout(LayoutKind.Sequential, Pack = 0x8)]
struct UserPresence struct UserPresence
{ {
public UserId UserId; public UserId UserId;
@@ -13,15 +16,20 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.FriendService
[MarshalAs(UnmanagedType.I1)] [MarshalAs(UnmanagedType.I1)]
public bool SamePresenceGroupApplication; public bool SamePresenceGroupApplication;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3)] public Array3<byte> Unknown;
public char[] Unknown; private AppKeyValueStorageHolder _appKeyValueStorage;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0xC0)] public Span<byte> AppKeyValueStorage => MemoryMarshal.Cast<AppKeyValueStorageHolder, byte>(MemoryMarshal.CreateSpan(ref _appKeyValueStorage, AppKeyValueStorageHolder.Size));
public char[] AppKeyValueStorage;
[StructLayout(LayoutKind.Sequential, Pack = 0x1, Size = Size)]
private struct AppKeyValueStorageHolder
{
public const int Size = 0xC0;
}
public override string ToString() public override string ToString()
{ {
return $"UserPresence {{ UserId: {UserId}, LastTimeOnlineTimestamp: {LastTimeOnlineTimestamp}, Status: {Status}, AppKeyValueStorage: {AppKeyValueStorage} }}"; return $"UserPresence {{ UserId: {UserId}, LastTimeOnlineTimestamp: {LastTimeOnlineTimestamp}, Status: {Status}, AppKeyValueStorage: {Encoding.ASCII.GetString(AppKeyValueStorage)} }}";
} }
} }
} }

View File

@@ -236,23 +236,14 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
ulong position = context.Request.PtrBuff[0].Position; ulong position = context.Request.PtrBuff[0].Position;
ulong size = context.Request.PtrBuff[0].Size; ulong size = context.Request.PtrBuff[0].Size;
byte[] bufferContent = new byte[size]; ReadOnlySpan<UserPresence> userPresenceInputArray = MemoryMarshal.Cast<byte, UserPresence>(context.Memory.GetSpan(position, (int)size));
context.Memory.Read(position, bufferContent);
if (uuid.IsNull) if (uuid.IsNull)
{ {
return ResultCode.InvalidArgument; return ResultCode.InvalidArgument;
} }
int elementCount = bufferContent.Length / Marshal.SizeOf<UserPresence>(); Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = uuid.ToString(), userPresenceInputArray = userPresenceInputArray.ToArray() });
using (BinaryReader bufferReader = new BinaryReader(new MemoryStream(bufferContent)))
{
UserPresence[] userPresenceInputArray = bufferReader.ReadStructArray<UserPresence>(elementCount);
Logger.Stub?.PrintStub(LogClass.ServiceFriend, new { UserId = uuid.ToString(), userPresenceInputArray });
}
return ResultCode.Success; return ResultCode.Success;
} }

View File

@@ -1,15 +1,13 @@
using System.Runtime.InteropServices; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator.NotificationService
{ {
[StructLayout(LayoutKind.Sequential, Pack = 0x8, Size = 0x10)] [StructLayout(LayoutKind.Sequential, Size = 0x10)]
struct NotificationInfo struct NotificationInfo
{ {
public NotificationEventType Type; public NotificationEventType Type;
private Array4<byte> _padding;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x4)]
public char[] Padding;
public long NetworkUserIdPlaceholder; public long NetworkUserIdPlaceholder;
} }
} }

View File

@@ -5,6 +5,7 @@ using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService.Types;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
@@ -75,7 +76,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
for (int i = 0; i < filteredApplicationPlayStatistics.Count(); i++) for (int i = 0; i < filteredApplicationPlayStatistics.Count(); i++)
{ {
MemoryHelper.Write(context.Memory, outputPosition + (ulong)(i * Marshal.SizeOf<ApplicationPlayStatistics>()), filteredApplicationPlayStatistics.ElementAt(i).Value); MemoryHelper.Write(context.Memory, outputPosition + (ulong)(i * Unsafe.SizeOf<ApplicationPlayStatistics>()), filteredApplicationPlayStatistics.ElementAt(i).Value);
} }
context.ResponseData.Write(filteredApplicationPlayStatistics.Count()); context.ResponseData.Write(filteredApplicationPlayStatistics.Count());

View File

@@ -1,4 +1,5 @@
using Ryujinx.HLE.HOS.Services.Time.TimeZone; using Ryujinx.HLE.HOS.Services.Time.TimeZone;
using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Time.Clock namespace Ryujinx.HLE.HOS.Services.Time.Clock
@@ -16,14 +17,22 @@ namespace Ryujinx.HLE.HOS.Services.Time.Clock
public CalendarAdditionalInfo NetworkCalendarAdditionalTime; public CalendarAdditionalInfo NetworkCalendarAdditionalTime;
public SteadyClockTimePoint SteadyClockTimePoint; public SteadyClockTimePoint SteadyClockTimePoint;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x24)] private LocationNameStorageHolder _locationName;
public char[] LocationName;
public Span<byte> LocationName => MemoryMarshal.Cast<LocationNameStorageHolder, byte>(MemoryMarshal.CreateSpan(ref _locationName, LocationNameStorageHolder.Size));
[MarshalAs(UnmanagedType.I1)] [MarshalAs(UnmanagedType.I1)]
public bool IsAutomaticCorrectionEnabled; public bool IsAutomaticCorrectionEnabled;
public byte Type; public byte Type;
public ushort Unknown; public ushort Unknown;
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)]
private struct LocationNameStorageHolder
{
public const int Size = 0x24;
}
public static ResultCode GetCurrentTime(out long currentTime, SteadyClockTimePoint steadyClockTimePoint, SystemClockContext context) public static ResultCode GetCurrentTime(out long currentTime, SteadyClockTimePoint steadyClockTimePoint, SystemClockContext context)
{ {
currentTime = 0; currentTime = 0;

View File

@@ -8,7 +8,9 @@ using Ryujinx.HLE.HOS.Services.Time.TimeZone;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
namespace Ryujinx.HLE.HOS.Services.Time namespace Ryujinx.HLE.HOS.Services.Time
{ {
@@ -281,7 +283,7 @@ namespace Ryujinx.HLE.HOS.Services.Time
{ {
byte type = context.RequestData.ReadByte(); byte type = context.RequestData.ReadByte();
context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize((uint)Marshal.SizeOf<ClockSnapshot>()); context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize((uint)Unsafe.SizeOf<ClockSnapshot>());
context.RequestData.BaseStream.Position += 7; context.RequestData.BaseStream.Position += 7;
@@ -372,12 +374,9 @@ namespace Ryujinx.HLE.HOS.Services.Time
return result; return result;
} }
char[] tzName = deviceLocationName.ToCharArray(); ReadOnlySpan<byte> tzName = Encoding.ASCII.GetBytes(deviceLocationName);
char[] locationName = new char[0x24];
Array.Copy(tzName, locationName, tzName.Length); tzName.CopyTo(clockSnapshot.LocationName);
clockSnapshot.LocationName = locationName;
result = ClockSnapshot.GetCurrentTime(out clockSnapshot.UserTime, currentTimePoint, clockSnapshot.UserContext); result = ClockSnapshot.GetCurrentTime(out clockSnapshot.UserTime, currentTimePoint, clockSnapshot.UserContext);
@@ -414,7 +413,7 @@ namespace Ryujinx.HLE.HOS.Services.Time
private ClockSnapshot ReadClockSnapshotFromBuffer(ServiceCtx context, IpcPtrBuffDesc ipcDesc) private ClockSnapshot ReadClockSnapshotFromBuffer(ServiceCtx context, IpcPtrBuffDesc ipcDesc)
{ {
Debug.Assert(ipcDesc.Size == (ulong)Marshal.SizeOf<ClockSnapshot>()); Debug.Assert(ipcDesc.Size == (ulong)Unsafe.SizeOf<ClockSnapshot>());
byte[] temp = new byte[ipcDesc.Size]; byte[] temp = new byte[ipcDesc.Size];

View File

@@ -5,6 +5,7 @@ using Ryujinx.HLE.Utilities;
using System; using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -890,14 +891,14 @@ namespace Ryujinx.HLE.HOS.Services.Time.TimeZone
long streamLength = reader.BaseStream.Length; long streamLength = reader.BaseStream.Length;
if (streamLength < Marshal.SizeOf<TzifHeader>()) if (streamLength < Unsafe.SizeOf<TzifHeader>())
{ {
return false; return false;
} }
TzifHeader header = reader.ReadStruct<TzifHeader>(); TzifHeader header = reader.ReadStruct<TzifHeader>();
streamLength -= Marshal.SizeOf<TzifHeader>(); streamLength -= Unsafe.SizeOf<TzifHeader>();
int ttisGMTCount = Detzcode32(header.TtisGMTCount); int ttisGMTCount = Detzcode32(header.TtisGMTCount);
int ttisSTDCount = Detzcode32(header.TtisSTDCount); int ttisSTDCount = Detzcode32(header.TtisSTDCount);

View File

@@ -1,4 +1,5 @@
using System.Runtime.InteropServices; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.Input.Motion.CemuHook.Protocol namespace Ryujinx.Input.Motion.CemuHook.Protocol
{ {
@@ -8,9 +9,7 @@ namespace Ryujinx.Input.Motion.CemuHook.Protocol
public MessageType Type; public MessageType Type;
public SubscriberType SubscriberType; public SubscriberType SubscriberType;
public byte Slot; public byte Slot;
public Array6<byte> MacAddress;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] MacAddress;
} }
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
@@ -27,11 +26,8 @@ namespace Ryujinx.Input.Motion.CemuHook.Protocol
public uint DPadAnalog; public uint DPadAnalog;
public ulong MainButtonsAnalog; public ulong MainButtonsAnalog;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] public Array6<byte> Touch1;
public byte[] Touch1; public Array6<byte> Touch2;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] Touch2;
public ulong MotionTimestamp; public ulong MotionTimestamp;
public float AccelerometerX; public float AccelerometerX;

View File

@@ -1,4 +1,5 @@
using System.Runtime.InteropServices; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.Input.Motion.CemuHook.Protocol namespace Ryujinx.Input.Motion.CemuHook.Protocol
{ {
@@ -14,8 +15,6 @@ namespace Ryujinx.Input.Motion.CemuHook.Protocol
{ {
public MessageType Type; public MessageType Type;
public int PortsCount; public int PortsCount;
public Array4<byte> PortIndices;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public byte[] PortIndices;
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Runtime.InteropServices; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.Input.Motion.CemuHook.Protocol namespace Ryujinx.Input.Motion.CemuHook.Protocol
{ {
@@ -11,8 +12,7 @@ namespace Ryujinx.Input.Motion.CemuHook.Protocol
public DeviceModelType ModelType; public DeviceModelType ModelType;
public ConnectionType ConnectionType; public ConnectionType ConnectionType;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] public Array6<byte> MacAddress;
public byte[] MacAddress;
public BatteryStatus BatteryStatus; public BatteryStatus BatteryStatus;
} }

View File

@@ -44,6 +44,11 @@ namespace Ryujinx.Memory.Tests
throw new NotImplementedException(); throw new NotImplementedException();
} }
public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
{
throw new NotImplementedException();
}
public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false) public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
{ {
throw new NotImplementedException(); throw new NotImplementedException();

View File

@@ -136,6 +136,14 @@ namespace Ryujinx.Memory
} }
} }
/// <inheritdoc/>
public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
{
Write(va, data);
return true;
}
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false) public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
{ {

View File

@@ -58,6 +58,17 @@ namespace Ryujinx.Memory
/// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception> /// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception>
void Write(ulong va, ReadOnlySpan<byte> data); void Write(ulong va, ReadOnlySpan<byte> data);
/// <summary>
/// Writes data to the application process, returning false if the data was not changed.
/// This triggers read memory tracking, as a redundancy check would be useless if the data is not up to date.
/// </summary>
/// <remarks>The memory manager can return that memory has changed when it hasn't to avoid expensive data copies.</remarks>
/// <param name="va">Virtual address to write the data into</param>
/// <param name="data">Data to be written</param>
/// <exception cref="InvalidMemoryRegionException">Throw for unhandled invalid or unmapped memory accesses</exception>
/// <returns>True if the data was changed, false otherwise</returns>
bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data);
void Fill(ulong va, ulong size, byte value) void Fill(ulong va, ulong size, byte value)
{ {
const int MaxChunkSize = 1 << 24; const int MaxChunkSize = 1 << 24;

View File

@@ -735,7 +735,6 @@ namespace Ryujinx.Ui
_emulationContext.Dispose(); _emulationContext.Dispose();
SwitchToGameTable(); SwitchToGameTable();
RendererWidget.Dispose();
return; return;
} }
@@ -747,7 +746,6 @@ namespace Ryujinx.Ui
_emulationContext.Dispose(); _emulationContext.Dispose();
SwitchToGameTable(); SwitchToGameTable();
RendererWidget.Dispose();
return; return;
} }
@@ -770,7 +768,6 @@ namespace Ryujinx.Ui
_emulationContext.Dispose(); _emulationContext.Dispose();
SwitchToGameTable(); SwitchToGameTable();
RendererWidget.Dispose();
return; return;
} }

View File

@@ -519,11 +519,15 @@ namespace Ryujinx.Ui
_gpuCancellationTokenSource.Cancel(); _gpuCancellationTokenSource.Cancel();
_isStopped = true; _isStopped = true;
if (_isActive)
{
_isActive = false; _isActive = false;
_exitEvent.WaitOne(); _exitEvent.WaitOne();
_exitEvent.Dispose(); _exitEvent.Dispose();
} }
}
private void NVStutterWorkaround() private void NVStutterWorkaround()
{ {

View File

@@ -72,7 +72,8 @@ namespace Ryujinx.Ui
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
Device.DisposeGpu(); Device?.DisposeGpu();
NpadManager.Dispose(); NpadManager.Dispose();
} }
} }