Compare commits

...

13 Commits

Author SHA1 Message Date
Shyanne
09d4bfe6d8 Merge branch 'master' into 20-empty-nca-lockup 2026-01-28 00:34:04 -05:00
GreemDev
5ed94c365b add a stack trace for the catch branch of AppleHardwareDeviceDriver.IsSupported 2026-01-27 17:52:45 -06:00
GreemDev
fef93a453a [ci skip] replace all usages of IntPtr with nint 2026-01-27 17:41:46 -06:00
Shyanne
2274a32813 Updated LoadAndSaveMetadata to return Optional type (but for real this time)
- fixed a typo i found
- awaiting dialog on invalid unpacked error
-
2025-12-30 17:06:21 -05:00
Shyanne
34190c9184 Moved null title id check to LoadAndSaveMetadata 2025-12-29 12:09:44 -05:00
Shyanne
18501d01f3 Merge branch '20-empty-nca-lockup' of https://git.ryujinx.app/sh0inx/ryujinx into 20-empty-nca-lockup 2025-12-29 00:31:45 -05:00
Shyanne
6780ff0d8a 👍 2025-12-29 00:28:50 -05:00
Shyanne
3cc02ebaef maybe don't dispose of the gpu when it doesnt exist 2025-12-28 16:54:21 -05:00
Shyanne
6938265651 Merge branch 'master' into 20-empty-nca-lockup 2025-12-28 14:16:39 -05:00
Shyanne
f6328ebb69 Amended typos and updated commentary 2025-12-28 13:16:07 -06:00
Shyanne
445924102e Addressed empty NCA lockup
- Updated LoadGuestApplication to use a CancellationTokenSource so we can properly asynchronously cancel and not hang (other things could listen to this too, or cancel it).
- Moved PrepareLoadScreen() to later in the pipeline (because cancelling LoadGuestApplication causes issues).
- Added Metadata read/write logging.
- Made AppHost inherit Disposable interface so that the garbage collector kicks in (side effect: made private Dispose() public)
- Added a WaitHandle to wait on either gpuDoneEvent or gpuCTS.Cancel event (LoadGuestApplication cancellation).
- Set invalid title ID for metadata.
2025-12-28 13:16:07 -06:00
Shyanne
cf72e189b7 Amended typos and updated commentary 2025-12-27 23:04:20 -05:00
Shyanne
8bd290cc57 Addressed empty NCA lockup
- Updated LoadGuestApplication to use a CancellationTokenSource so we can properly asynchronously cancel and not hang (other things could listen to this too, or cancel it).
- Moved PrepareLoadScreen() to later in the pipeline (because cancelling LoadGuestApplication causes issues).
- Added Metadata read/write logging.
- Made AppHost inherit Disposable interface so that the garbage collector kicks in (side effect: made private Dispose() public)
- Added a WaitHandle to wait on either gpuDoneEvent or gpuCTS.Cancel event (LoadGuestApplication cancellation).
- Set invalid title ID for metadata.
2025-12-27 22:34:48 -05:00
20 changed files with 185 additions and 113 deletions

View File

@@ -168,7 +168,7 @@ namespace ARMeilleure.Common
{
_allocated.Dispose();
foreach (IntPtr page in _pages.Values)
foreach (nint page in _pages.Values)
{
NativeAllocator.Instance.Free((void*)page);
}

View File

@@ -44,12 +44,12 @@ namespace Ryujinx.Audio.Backends.Apple
int result = AudioQueueNewOutput(
ref format,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero,
nint.Zero,
nint.Zero,
nint.Zero,
nint.Zero,
0,
out IntPtr testQueue);
out nint testQueue);
if (result == 0)
{
@@ -95,12 +95,12 @@ namespace Ryujinx.Audio.Backends.Apple
GetAudioFormat(SampleFormat.PcmInt16, Constants.TargetSampleRate, 2);
int result = AudioQueueNewOutput(
ref format,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero,
nint.Zero,
nint.Zero,
nint.Zero,
nint.Zero,
0,
out IntPtr testQueue);
out nint testQueue);
if (result == 0)
{
@@ -110,8 +110,9 @@ namespace Ryujinx.Audio.Backends.Apple
return false;
}
catch
catch (Exception e)
{
Logger.Error?.Print(LogClass.Audio, $"Failed to check if AudioToolbox is supported: {e.Message}\n{e.StackTrace}");
return false;
}
}

View File

@@ -1,15 +1,12 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using System.Runtime.Versioning;
using Ryujinx.Audio.Backends.Apple.Native;
using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox;
using static Ryujinx.Audio.Backends.Apple.AppleHardwareDeviceDriver;
namespace Ryujinx.Audio.Backends.Apple
{
@@ -27,8 +24,8 @@ namespace Ryujinx.Audio.Backends.Apple
private readonly AudioQueueOutputCallback _callbackDelegate;
private readonly GCHandle _gcHandle;
private IntPtr _audioQueue;
private readonly IntPtr[] _audioQueueBuffers = new IntPtr[NumBuffers];
private nint _audioQueue;
private readonly nint[] _audioQueueBuffers = new nint[NumBuffers];
private readonly int[] _bufferBytesFilled = new int[NumBuffers];
private readonly int _bytesPerFrame;
@@ -41,9 +38,9 @@ namespace Ryujinx.Audio.Backends.Apple
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void AudioQueueOutputCallback(
IntPtr userData,
IntPtr audioQueue,
IntPtr buffer);
nint userData,
nint audioQueue,
nint buffer);
public AppleHardwareDeviceSession(
AppleHardwareDeviceDriver driver,
@@ -72,15 +69,15 @@ namespace Ryujinx.Audio.Backends.Apple
RequestedSampleRate,
RequestedChannelCount);
IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate);
IntPtr userData = GCHandle.ToIntPtr(_gcHandle);
nint callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate);
nint userData = GCHandle.ToIntPtr(_gcHandle);
int result = AudioQueueNewOutput(
ref format,
callbackPtr,
userData,
IntPtr.Zero,
IntPtr.Zero,
nint.Zero,
nint.Zero,
0,
out _audioQueue);
@@ -102,7 +99,7 @@ namespace Ryujinx.Audio.Backends.Apple
}
}
private unsafe void PrimeBuffer(IntPtr bufferPtr, int bufferIndex)
private unsafe void PrimeBuffer(nint bufferPtr, int bufferIndex)
{
AudioQueueBuffer* buffer = (AudioQueueBuffer*)bufferPtr;
@@ -126,12 +123,12 @@ namespace Ryujinx.Audio.Backends.Apple
buffer->AudioDataByteSize = (uint)capacityBytes;
_bufferBytesFilled[bufferIndex] = bytesToRead;
AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, IntPtr.Zero);
AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, nint.Zero);
}
private void OutputCallback(IntPtr userData, IntPtr audioQueue, IntPtr bufferPtr)
private void OutputCallback(nint userData, nint audioQueue, nint bufferPtr)
{
if (!_started || bufferPtr == IntPtr.Zero)
if (!_started || bufferPtr == nint.Zero)
return;
int bufferIndex = Array.IndexOf(_audioQueueBuffers, bufferPtr);
@@ -176,7 +173,7 @@ namespace Ryujinx.Audio.Backends.Apple
}
}
private unsafe void ApplyVolume(IntPtr dataPtr, int byteSize)
private unsafe void ApplyVolume(nint dataPtr, int byteSize)
{
float volume = Math.Clamp(_volume * _driver.Volume, 0f, 1f);
if (volume >= 0.999f)
@@ -226,7 +223,7 @@ namespace Ryujinx.Audio.Backends.Apple
return;
_started = true;
AudioQueueStart(_audioQueue, IntPtr.Zero);
AudioQueueStart(_audioQueue, nint.Zero);
}
}
@@ -265,11 +262,11 @@ namespace Ryujinx.Audio.Backends.Apple
{
Stop();
if (_audioQueue != IntPtr.Zero)
if (_audioQueue != nint.Zero)
{
AudioQueueStop(_audioQueue, true);
AudioQueueDispose(_audioQueue, true);
_audioQueue = IntPtr.Zero;
_audioQueue = nint.Zero;
}
if (_gcHandle.IsAllocated)

View File

@@ -130,7 +130,7 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native
unsafe
{
int* frameCountPtr = &nativeFrameCount;
IntPtr* arenasPtr = &arenas;
nint* arenasPtr = &arenas;
CheckError(soundio_outstream_begin_write(_context, (nint)arenasPtr, (nint)frameCountPtr));
frameCount = *frameCountPtr;

View File

@@ -30,9 +30,9 @@ namespace ARMeilleure.Common
/// <summary>
/// Base address for the page.
/// </summary>
public readonly IntPtr Address;
public readonly nint Address;
public AddressTablePage(bool isSparse, IntPtr address)
public AddressTablePage(bool isSparse, nint address)
{
IsSparse = isSparse;
Address = address;
@@ -47,20 +47,20 @@ namespace ARMeilleure.Common
public readonly SparseMemoryBlock Block;
private readonly TrackingEventDelegate _trackingEvent;
public TableSparseBlock(ulong size, Action<IntPtr> ensureMapped, PageInitDelegate pageInit)
public TableSparseBlock(ulong size, Action<nint> ensureMapped, PageInitDelegate pageInit)
{
SparseMemoryBlock block = new(size, pageInit, null);
_trackingEvent = (address, size, write) =>
{
ulong pointer = (ulong)block.Block.Pointer + address;
ensureMapped((IntPtr)pointer);
ensureMapped((nint)pointer);
return pointer;
};
bool added = NativeSignalHandler.AddTrackedRegion(
(nuint)block.Block.Pointer,
(nuint)(block.Block.Pointer + (IntPtr)block.Block.Size),
(nuint)(block.Block.Pointer + (nint)block.Block.Size),
Marshal.GetFunctionPointerForDelegate(_trackingEvent));
if (!added)
@@ -116,7 +116,7 @@ namespace ARMeilleure.Common
}
/// <inheritdoc/>
public IntPtr Base
public nint Base
{
get
{
@@ -124,7 +124,7 @@ namespace ARMeilleure.Common
lock (_pages)
{
return (IntPtr)GetRootPage();
return (nint)GetRootPage();
}
}
}
@@ -240,7 +240,7 @@ namespace ARMeilleure.Common
long index = Levels[^1].GetValue(address);
EnsureMapped((IntPtr)(page + index));
EnsureMapped((nint)(page + index));
return ref page[index];
}
@@ -284,7 +284,7 @@ namespace ARMeilleure.Common
/// Ensure the given pointer is mapped in any overlapping sparse reservations.
/// </summary>
/// <param name="ptr">Pointer to be mapped</param>
private void EnsureMapped(IntPtr ptr)
private void EnsureMapped(nint ptr)
{
if (Sparse)
{
@@ -299,7 +299,7 @@ namespace ARMeilleure.Common
{
SparseMemoryBlock sparse = reserved.Block;
if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (IntPtr)sparse.Block.Size)
if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (nint)sparse.Block.Size)
{
sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer));
@@ -319,15 +319,15 @@ namespace ARMeilleure.Common
/// </summary>
/// <param name="level">Level to get the fill value for</param>
/// <returns>The fill value</returns>
private IntPtr GetFillValue(int level)
private nint GetFillValue(int level)
{
if (_fillBottomLevel != null && level == Levels.Length - 2)
{
return (IntPtr)_fillBottomLevelPtr;
return (nint)_fillBottomLevelPtr;
}
else
{
return IntPtr.Zero;
return nint.Zero;
}
}
@@ -379,7 +379,7 @@ namespace ARMeilleure.Common
/// <param name="fill">Fill value</param>
/// <param name="leaf"><see langword="true"/> if leaf; otherwise <see langword="false"/></param>
/// <returns>Allocated block</returns>
private IntPtr Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
private nint Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
{
int size = sizeof(T) * length;
@@ -405,7 +405,7 @@ namespace ARMeilleure.Common
}
}
page = new AddressTablePage(true, block.Block.Pointer + (IntPtr)_sparseReservedOffset);
page = new AddressTablePage(true, block.Block.Pointer + (nint)_sparseReservedOffset);
_sparseReservedOffset += (ulong)size;
@@ -413,7 +413,7 @@ namespace ARMeilleure.Common
}
else
{
IntPtr address = (IntPtr)NativeAllocator.Instance.Allocate((uint)size);
nint address = (nint)NativeAllocator.Instance.Allocate((uint)size);
page = new AddressTablePage(false, address);
Span<T> span = new((void*)page.Address, length);

View File

@@ -658,7 +658,7 @@ namespace Ryujinx.Graphics.Gpu.Image
bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel;
IntPtr hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0;
nint hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0;
if (hostPointer != 0 && _context.Renderer.PrepareHostMapping(hostPointer, Storage.Size))
{

View File

@@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using System;
using System.Diagnostics;
using System.Threading;
@@ -114,7 +115,7 @@ namespace Ryujinx.Graphics.Vulkan
cbs.AddDependant(this);
// We need to add a dependency on the command buffer to all objects this object
// references aswell.
// references as well.
if (_referencedObjs != null)
{
for (int i = 0; i < _referencedObjs.Length; i++)
@@ -176,6 +177,8 @@ namespace Ryujinx.Graphics.Vulkan
}
}
// This can somehow become -1.
// Logger.Info?.PrintMsg(LogClass.Gpu, $"_referenceCount: {_referenceCount}");
Debug.Assert(_referenceCount >= 0);
}

View File

@@ -19,7 +19,7 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK
public static void Initialize()
{
IntPtr configSize = (nint)Marshal.SizeOf<MVKConfiguration>();
nint configSize = (nint)Marshal.SizeOf<MVKConfiguration>();
vkGetMoltenVKConfigurationMVK(nint.Zero, out MVKConfiguration config, configSize);

View File

@@ -86,7 +86,7 @@ namespace Ryujinx.Graphics.Vulkan
enabledExtensions = enabledExtensions.Append(ExtDebugUtils.ExtensionName).ToArray();
}
IntPtr appName = Marshal.StringToHGlobalAnsi(AppName);
nint appName = Marshal.StringToHGlobalAnsi(AppName);
ApplicationInfo applicationInfo = new()
{
@@ -166,7 +166,7 @@ namespace Ryujinx.Graphics.Vulkan
internal static DeviceInfo[] GetSuitablePhysicalDevices(Vk api)
{
IntPtr appName = Marshal.StringToHGlobalAnsi(AppName);
nint appName = Marshal.StringToHGlobalAnsi(AppName);
ApplicationInfo applicationInfo = new()
{

View File

@@ -1059,7 +1059,7 @@ namespace Ryujinx.HLE.FileSystem
}
}
public static bool AreKeysAlredyPresent(string pathToCheck)
public static bool AreKeysAlreadyPresent(string pathToCheck)
{
string[] fileNames = ["prod.keys", "title.keys", "console.keys", "dev.keys"];
foreach (string file in fileNames)

View File

@@ -21,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; }
public nint Handle => IntPtr.Zero;
public nint Handle => nint.Zero;
public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint;

View File

@@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Path = System.IO.Path;
@@ -27,10 +28,15 @@ namespace Ryujinx.HLE.Loaders.Processes
private ulong _latestPid;
public ProcessResult ActiveApplication
public ProcessResult? ActiveApplication
{
get
{
return _processesByPid.GetValueOrDefault(_latestPid);
// Using this if statement locks up the UI and prevents a new game from loading.
// Haven't quite deduced why yet.
if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value))
throw new RyujinxException(
$"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?");
@@ -144,7 +150,7 @@ namespace Ryujinx.HLE.Loaders.Processes
public bool LoadUnpackedNca(string exeFsDirPath, string romFsPath = null)
{
ProcessResult processResult = new LocalFileSystem(exeFsDirPath).Load(_device, romFsPath);
if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult))
{
if (processResult.Start(_device))

View File

@@ -4,7 +4,6 @@ using LibHac.Ns;
using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Horizon.Common;
namespace Ryujinx.HLE.Loaders.Processes
@@ -52,6 +51,7 @@ namespace Ryujinx.HLE.Loaders.Processes
if (metaLoader is not null)
{
Logger.Info?.Print(LogClass.Application,$"metaLoader: {metaLoader}");
ulong programId = metaLoader.ProgramId;
Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString();
@@ -71,8 +71,15 @@ namespace Ryujinx.HLE.Loaders.Processes
ProgramId = programId;
ProgramIdText = $"{programId:x16}";
Is64Bit = metaLoader.IsProgram64Bit;
}
else
{
Logger.Error?.Print(LogClass.Application,$"metaLoader is null !!!");
ProcessId = 0;
return;
}
DiskCacheEnabled = diskCacheEnabled;
AllowCodeMemoryForJit = allowCodeMemoryForJit;
}

View File

@@ -159,7 +159,7 @@ namespace Ryujinx.Memory.WindowsShared
{
SplitForMap((ulong)location, (ulong)size, srcOffset);
IntPtr ptr = WindowsApi.MapViewOfFile3(
nint ptr = WindowsApi.MapViewOfFile3(
sharedMemory,
WindowsApi.CurrentProcessHandle,
location,

View File

@@ -227,7 +227,7 @@ namespace Ryujinx.Tests.Memory
// Create some info to be used for managing the native writing loop.
int stateSize = Unsafe.SizeOf<NativeWriteLoopState>();
IntPtr statePtr = Marshal.AllocHGlobal(stateSize);
nint statePtr = Marshal.AllocHGlobal(stateSize);
Unsafe.InitBlockUnaligned((void*)statePtr, 0, (uint)stateSize);
ref NativeWriteLoopState writeLoopState = ref Unsafe.AsRef<NativeWriteLoopState>((void*)statePtr);

View File

@@ -62,7 +62,7 @@ using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
namespace Ryujinx.Ava.Systems
{
internal class AppHost
internal class AppHost : IDisposable
{
private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
@@ -438,7 +438,7 @@ namespace Ryujinx.Ava.Systems
SaveBitmapAsPng(bitmapToSave, path);
Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
Logger.Notice.Print(LogClass.Application, $"Screenshot saved to '{path}'.", "Screenshot");
}
});
}
@@ -611,27 +611,40 @@ namespace Ryujinx.Ava.Systems
_isActive = false;
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
_gpuDoneEvent.WaitOne();
_gpuDoneEvent.Dispose();
DisplaySleep.Restore();
NpadManager.Dispose();
TouchScreenManager.Dispose();
Device.Dispose();
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
// We only need to wait for all commands submitted during the main gpu loop to be processed.
// If the GPU has no work and is cancelled, we need to handle that as well.
WaitHandle.WaitAny(new[] { _gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle });
_gpuCancellationTokenSource.Dispose();
// Waiting for work to be finished before we dispose.
if (_renderingStarted)
{
Device.Gpu.WaitUntilGpuReady();
}
_gpuDoneEvent.Dispose();
DisposeGpu();
AppExit?.Invoke(this, EventArgs.Empty);
}
private void Dispose()
// MUST be public to inherit from IDisposable
public void Dispose()
{
if (Device.Processes != null)
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed);
{
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText,
_playTimer.Elapsed);
}
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
@@ -646,7 +659,6 @@ namespace Ryujinx.Ava.Systems
_topLevel.PointerExited -= TopLevel_PointerExited;
_gpuCancellationTokenSource.Cancel();
_gpuCancellationTokenSource.Dispose();
_chrono.Stop();
_playTimer.Stop();
@@ -672,6 +684,12 @@ namespace Ryujinx.Ava.Systems
}
else
{
// No use waiting on something that never started work
if (_renderingStarted)
{
Device.Gpu.WaitUntilGpuReady();
}
Device.DisposeGpu();
}
}
@@ -686,7 +704,7 @@ namespace Ryujinx.Ava.Systems
_cursorState = CursorStates.ForceChangeCursor;
}
public async Task<bool> LoadGuestApplication(BlitStruct<ApplicationControlProperty>? customNacpData = null)
public async Task LoadGuestApplication(CancellationTokenSource cts, BlitStruct<ApplicationControlProperty>? customNacpData = null)
{
DiscordIntegrationModule.GuestAppStartedAt = Timestamps.Now;
@@ -715,7 +733,8 @@ namespace Ryujinx.Ava.Systems
await UserErrorDialog.ShowUserErrorDialog(userError);
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
@@ -724,10 +743,11 @@ namespace Ryujinx.Ava.Systems
await UserErrorDialog.ShowUserErrorDialog(userError);
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
// Tell the user that we installed a firmware for them.
// Tell the user that we installed firmware for them.
if (userError is UserError.NoFirmware)
{
firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
@@ -747,7 +767,8 @@ namespace Ryujinx.Ava.Systems
await UserErrorDialog.ShowUserErrorDialog(userError);
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
}
@@ -762,7 +783,8 @@ namespace Ryujinx.Ava.Systems
{
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
else if (Directory.Exists(ApplicationPath))
@@ -782,20 +804,24 @@ namespace Ryujinx.Ava.Systems
if (!Device.LoadCart(ApplicationPath, romFsFiles[0]))
{
await ContentDialogHelper.CreateErrorDialog(
"Please specify an unpacked game directory with a valid exefs or NSO/NRO.");
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
else
{
Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
if (!Device.LoadCart(ApplicationPath))
{
await ContentDialogHelper.CreateErrorDialog(
"Please specify an unpacked game directory with a valid exefs or NSO/NRO.");
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
}
@@ -813,7 +839,8 @@ namespace Ryujinx.Ava.Systems
{
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
break;
@@ -826,7 +853,8 @@ namespace Ryujinx.Ava.Systems
{
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
break;
@@ -840,7 +868,8 @@ namespace Ryujinx.Ava.Systems
{
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
break;
@@ -855,7 +884,8 @@ namespace Ryujinx.Ava.Systems
{
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
}
catch (ArgumentOutOfRangeException)
@@ -864,7 +894,8 @@ namespace Ryujinx.Ava.Systems
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
break;
@@ -873,19 +904,18 @@ namespace Ryujinx.Ava.Systems
}
else
{
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NSO/NRO file.");
Device.Dispose();
return false;
cts.Cancel();
throw new OperationCanceledException(cts.Token);
}
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText,
appMetadata => appMetadata.UpdatePreGame()
);
_playTimer.Start();
return true;
}
internal void Resume()
@@ -895,7 +925,7 @@ namespace Ryujinx.Ava.Systems
_viewModel.IsPaused = false;
_playTimer.Start();
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI);
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed.");
}
internal void Pause()
@@ -905,7 +935,7 @@ namespace Ryujinx.Ava.Systems
_viewModel.IsPaused = true;
_playTimer.Stop();
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]);
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused.");
}
private void InitEmulatedSwitch()
@@ -1104,7 +1134,9 @@ namespace Ryujinx.Ava.Systems
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands...");
threaded.FlushThreadedCommands();
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!");
}
_gpuDoneEvent.Set();

View File

@@ -849,7 +849,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary
foreach (ApplicationData installedApplication in Applications.Items)
{
temporary += LoadAndSaveMetaData(installedApplication.IdString).TimePlayed;
// this should always exist... should...
temporary += LoadAndSaveMetaData(installedApplication.IdString).Value.TimePlayed;
}
TotalTimePlayed = temporary;
@@ -1159,15 +1160,22 @@ namespace Ryujinx.Ava.Systems.AppLibrary
ApplicationCountUpdated?.Invoke(null, e);
}
public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null)
public static Gommon.Optional<ApplicationMetadata> LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null)
{
if (titleId is null)
{
Logger.Warning?.PrintMsg(LogClass.Application, "Cannot save metadata because title ID is invalid.");
return null;
}
string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui");
string metadataFile = Path.Combine(metadataFolder, "metadata.json");
ApplicationMetadata appMetadata;
if (!File.Exists(metadataFile))
{
Logger.Info?.Print(LogClass.Application, $"Metadata file does not exist. Creating metadata for {titleId}...");
Directory.CreateDirectory(metadataFolder);
appMetadata = new ApplicationMetadata();
@@ -1177,12 +1185,12 @@ namespace Ryujinx.Ava.Systems.AppLibrary
try
{
Logger.Debug?.Print(LogClass.Application, $"Deserializing metadata for {titleId}...");
appMetadata = JsonHelper.DeserializeFromFile(metadataFile, _serializerContext.ApplicationMetadata);
}
catch (JsonException)
{
Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults.");
appMetadata = new ApplicationMetadata();
}

View File

@@ -82,7 +82,7 @@ namespace Ryujinx.Ava.Systems
public static void Use(Optional<string> titleId)
{
if (titleId.TryGet(out string tid))
if (titleId.TryGet(out string tid) && Switch.Shared.Processes.ActiveApplication is not null)
SwitchToPlayingState(
ApplicationLibrary.LoadAndSaveMetaData(tid),
Switch.Shared.Processes.ActiveApplication

View File

@@ -57,8 +57,15 @@ namespace Ryujinx.Ava.UI.Models
}
else
{
ApplicationMetadata appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString);
Title = appMetadata.Title ?? TitleIdString;
Gommon.Optional<ApplicationMetadata> appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString);
if (appMetadata != null)
{
Title = appMetadata.Value.Title ?? TitleIdString;
}
else
{
Title = "<INVALID>";
}
}
Task.Run(() =>

View File

@@ -1029,7 +1029,7 @@ namespace Ryujinx.Ava.UI.ViewModels
string dialogMessage =
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallMessage);
if (ContentManager.AreKeysAlredyPresent(systemDirectory))
if (ContentManager.AreKeysAlreadyPresent(systemDirectory))
{
dialogMessage +=
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys
@@ -1703,11 +1703,6 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.RestartTime();
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path,
ConfigurationState.Instance.System.Language, application.Id);
PrepareLoadScreen();
RendererHostControl = new RendererHost();
AppHost = new AppHost(
@@ -1721,18 +1716,34 @@ namespace Ryujinx.Ava.UI.ViewModels
UserChannelPersistence,
this,
TopLevel);
CancellationTokenSource cts = new CancellationTokenSource();
if (!await AppHost.LoadGuestApplication(customNacpData))
try
{
await AppHost.LoadGuestApplication(cts, customNacpData);
}
catch (OperationCanceledException exception)
{
Logger.Info?.Print(LogClass.Application,
"LoadGuestApplication was interrupted !!! " + exception.Message);
AppHost.DisposeContext();
AppHost = null;
return;
}
finally
{
cts.Dispose();
}
CanUpdate = false;
application.Name ??= AppHost.Device.Processes.ActiveApplication.Name;
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path,
ConfigurationState.Instance.System.Language, application.Id);
PrepareLoadScreen();
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, application.Name);
@@ -1754,9 +1765,9 @@ namespace Ryujinx.Ava.UI.ViewModels
RendererHostControl.Focus();
});
public static void UpdateGameMetadata(string titleId, TimeSpan playTime)
public static void UpdateGameMetadata(string titleId, TimeSpan playTime)
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime));
public void RefreshFirmwareStatus()
{
SystemVersion version = null;