Compare commits

...

16 Commits

Author SHA1 Message Date
Shyanne
08ab4979d7 Merge branch 'master' into 20-empty-nca-lockup 2026-03-14 12:16:50 -04:00
KeatonTheBot
4b42087bd4 Linux: Fix file picker not launching from disabling core dumps (ryubing/ryujinx!249)
See merge request ryubing/ryujinx!249
2026-03-06 19:04:42 -06:00
EscoDev
80cbf5d1fc Fix incorrect save button locale in user editor (ryubing/ryujinx!280)
See merge request ryubing/ryujinx!280
2026-03-01 15:48:29 -06:00
LotP
cc6d2dc162 fix nacp language buffer (ryubing/ryujinx!281)
See merge request ryubing/ryujinx!281
2026-02-25 13:58:31 -06:00
Daenorth
4ebc318da5 Add new RPC images (ryubing/ryujinx!279)
See merge request ryubing/ryujinx!279
2026-02-23 20:57:19 -06:00
Shyanne
09d4bfe6d8 Merge branch 'master' into 20-empty-nca-lockup 2026-01-28 00:34:04 -05: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
21 changed files with 274 additions and 75 deletions

View File

@@ -41,7 +41,7 @@
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.2-build3" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.126" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.129" />
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
<PackageVersion Include="Gommon" Version="2.8.0.1" />

View File

@@ -11300,6 +11300,31 @@
"zh_TW": "刪除"
}
},
{
"ID": "UserProfilesSave",
"Translations": {
"ar_SA": "",
"de_DE": "Speichern",
"el_GR": "",
"en_US": "Save",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "UserProfilesClose",
"Translations": {

View File

@@ -184,6 +184,7 @@ namespace Ryujinx.Common
"01001b300b9be000", // Diablo III: Eternal Collection
"010027400cdc6000", // Divinity Original 2 - Definitive Edition
"01008c8012920000", // Dying Light Platinum Edition
"0100d11013e6a000", // Eschatos
"01001cc01b2d4000", // Goat Simulator 3
"01003620068ea000", // Hand of Fate 2
"0100f7e00c70e000", // Hogwarts Legacy
@@ -193,9 +194,15 @@ namespace Ryujinx.Common
"0100d71004694000", // Minecraft
"01007430037f6000", // Monopoly
"0100853015e86000", // No Man's Sky
"0100f85014ed0000", // No More Heroes
"0100463014ed4000", // No More Heroes 2
"0100e570094e8000", // Owlboy
"01007bb017812000", // Portal
"0100abd01785c000", // Portal 2
"01009f100bc52000", // Psikyo Collection 1
"01009d400c4a8000", // Psikyo Collection 2
"01008e200c5c2000", // Muse Dash
"01005ff002e2a000", // Rayman Legends
"01007820196a6000", // Red Dead Redemption
"0100e8300a67a000", // Risk
"01002f7013224000", // Rune Factory 5

View File

@@ -22,10 +22,11 @@ namespace Ryujinx.Common.Utilities
}
// "dumpable" attribute of the calling process
private const int PR_GET_DUMPABLE = 3;
private const int PR_SET_DUMPABLE = 4;
[DllImport("libc", SetLastError = true)]
private static extern int prctl(int option, int arg2);
[LibraryImport("libc", SetLastError = true)]
private static partial int prctl(int option, int arg2);
public static void SetCoreDumpable(bool dumpable)
{
@@ -36,5 +37,13 @@ namespace Ryujinx.Common.Utilities
Debug.Assert(result == 0);
}
}
// Use the below line to display dumpable status in the console:
// Console.WriteLine($"{OsUtils.IsCoreDumpable()}");
public static bool IsCoreDumpable()
{
int result = prctl(PR_GET_DUMPABLE, 0);
return result == 1;
}
}
}

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

@@ -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

@@ -20,5 +20,7 @@ namespace Ryujinx.HLE.HOS.SystemState
SimplifiedChinese,
TraditionalChinese,
BrazilianPortuguese,
Polish,
Thai,
}
}

View File

@@ -23,7 +23,9 @@ namespace Ryujinx.HLE.HOS.SystemState
"es-419",
"zh-Hans",
"zh-Hant",
"pt-BR"
"pt-BR",
"pl",
"th"
];
internal long DesiredKeyboardLayout { get; private set; }

View File

@@ -18,5 +18,7 @@ namespace Ryujinx.HLE.HOS.SystemState
TraditionalChinese,
SimplifiedChinese,
BrazilianPortuguese,
Polish,
Thai,
}
}

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

@@ -1,12 +1,19 @@
using Ryujinx.Common.Memory;
using System;
using System.Buffers.Binary;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
namespace Ryujinx.Horizon.Sdk.Ns
{
public struct ApplicationControlProperty
{
public Array16<ApplicationTitle> Title;
/// <summary>
/// Use <see cref="Title"/> to access titles instead of accessing them directly.
/// </summary>
public Array16<ApplicationTitle> TitleBlock;
public Array37<byte> Isbn;
public StartupUserAccountValue StartupUserAccount;
public UserAccountSwitchLockValue UserAccountSwitchLock;
@@ -58,7 +65,10 @@ namespace Ryujinx.Horizon.Sdk.Ns
public RepairFlagValue RepairFlag;
public byte ProgramIndex;
public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag;
public Array4<byte> Reserved3214;
public byte ApplicationErrorCodePrefix;
public TitleCompressionValue TitleCompression;
public byte AcdIndex;
public byte ApparentPlatform;
public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration;
public ApplicationJitConfiguration JitConfiguration;
public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors;
@@ -74,6 +84,47 @@ namespace Ryujinx.Horizon.Sdk.Ns
public readonly string DisplayVersionString => Encoding.UTF8.GetString(DisplayVersion.AsSpan()).TrimEnd('\0');
public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0');
public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0');
private const int TitleCount = 32;
private const int TitleEntrySize = 0x300;
/// <summary>
/// Returns the resolved title entries. When <see cref="TitleCompression"/> is
/// <see cref="TitleCompressionValue.Enable"/>, the raw <see cref="TitleBlock"/> bytes are
/// decompressed (raw deflate) from 0x3000 into 0x6000 bytes yielding up to 32 entries.
/// Otherwise the 16 uncompressed entries from <see cref="TitleBlock"/> are returned directly.
/// </summary>
public readonly ApplicationTitle[] Title
{
get
{
var titles = new ApplicationTitle[TitleCount];
if (TitleCompression != TitleCompressionValue.Enable)
{
TitleBlock.AsSpan().CopyTo(titles);
return titles;
}
ReadOnlySpan<byte> titleBytes = MemoryMarshal.AsBytes(TitleBlock.AsSpan());
ushort compressedBlobSize = BinaryPrimitives.ReadUInt16LittleEndian(titleBytes);
ReadOnlySpan<byte> compressedBlob = titleBytes.Slice(2, compressedBlobSize);
byte[] decompressed = new byte[TitleCount * TitleEntrySize];
using (var compressedStream = new MemoryStream(compressedBlob.ToArray()))
using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
{
deflateStream.ReadExactly(decompressed, 0, decompressed.Length);
}
MemoryMarshal.Cast<byte, ApplicationTitle>(decompressed).CopyTo(titles);
return titles;
}
}
public struct ApplicationTitle
{
@@ -130,6 +181,8 @@ namespace Ryujinx.Horizon.Sdk.Ns
TraditionalChinese = 13,
SimplifiedChinese = 14,
BrazilianPortuguese = 15,
Polish = 16,
Thai = 17,
}
public enum Organization
@@ -302,5 +355,11 @@ namespace Ryujinx.Horizon.Sdk.Ns
Deny = 0,
Allow = 1,
}
public enum TitleCompressionValue : byte
{
Disable = 0,
Enable = 1,
}
}
}

View File

@@ -42,6 +42,7 @@ namespace Ryujinx.Ava
public static bool PreviewerDetached { get; private set; }
public static bool UseHardwareAcceleration { get; private set; }
public static string BackendThreadingArg { get; private set; }
public static bool CoreDumpArg { get; private set; }
private const uint MbIconwarning = 0x30;
@@ -81,6 +82,8 @@ namespace Ryujinx.Ava
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
CoreDumpArg = coreDumpArg;
// TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception.
// This is undesirable and causes very odd behavior during development (the process stops responding,
// the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user.

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();
}
@@ -1404,7 +1412,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
if (string.IsNullOrWhiteSpace(data.Name))
{
foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
foreach (ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
{
if (!controlTitle.NameString.IsEmpty())
{
@@ -1417,7 +1425,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
if (string.IsNullOrWhiteSpace(data.Developer))
{
foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
foreach (ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
{
if (!controlTitle.PublisherString.IsEmpty())
{

View File

@@ -24,6 +24,8 @@ namespace Ryujinx.Ava.Systems.Configuration.System
SimplifiedChinese,
TraditionalChinese,
BrazilianPortuguese,
Polish,
Thai,
}
public static class LanguageEnumHelper

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

@@ -1062,7 +1062,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
@@ -1736,11 +1736,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(
@@ -1754,18 +1749,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);
@@ -1787,9 +1798,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;

View File

@@ -108,7 +108,7 @@
<Button
Name="SaveButton"
Click="SaveButton_Click">
<TextBlock Text="{ext:Locale UserProfilesSetProfileImage}" />
<TextBlock Text="{ext:Locale UserProfilesSave}" />
</Button>
</StackPanel>
</Grid>

View File

@@ -1,5 +1,7 @@
using Avalonia.Platform.Storage;
using Gommon;
using Ryujinx.Common.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -11,29 +13,42 @@ namespace Ryujinx.Ava.Utilities
extension(IStorageProvider storageProvider)
{
public Task<Optional<IStorageFolder>> OpenSingleFolderPickerAsync(FolderPickerOpenOptions openOptions = null) =>
storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, false))
CoreDumpable(() => storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, false)))
.Then(folders => folders.FindFirst());
public Task<Optional<IStorageFile>> OpenSingleFilePickerAsync(FilePickerOpenOptions openOptions = null) =>
storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, false))
CoreDumpable(() => storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, false)))
.Then(files => files.FindFirst());
public Task<Optional<IReadOnlyList<IStorageFolder>>> OpenMultiFolderPickerAsync(FolderPickerOpenOptions openOptions = null) =>
storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, true))
CoreDumpable(() => storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, true)))
.Then(folders => folders.Count > 0 ? Optional.Of(folders) : default);
public Task<Optional<IReadOnlyList<IStorageFile>>> OpenMultiFilePickerAsync(FilePickerOpenOptions openOptions = null) =>
storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, true))
CoreDumpable(() => storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, true)))
.Then(files => files.Count > 0 ? Optional.Of(files) : default);
}
private static async Task<T> CoreDumpable<T>(Func<Task<T>> picker)
{
OsUtils.SetCoreDumpable(true);
try
{
return await picker();
}
finally
{
if (!Program.CoreDumpArg)
OsUtils.SetCoreDumpable(false);
}
}
private static FilePickerOpenOptions FixOpenOptions(this FilePickerOpenOptions openOptions, bool allowMultiple)
{
if (openOptions is null)
return new FilePickerOpenOptions { AllowMultiple = allowMultiple };
openOptions.AllowMultiple = allowMultiple;
return openOptions;
}
@@ -43,7 +58,6 @@ namespace Ryujinx.Ava.Utilities
return new FolderPickerOpenOptions { AllowMultiple = allowMultiple };
openOptions.AllowMultiple = allowMultiple;
return openOptions;
}
}