HLE: Refactoring of ApplicationLoader (#4480)

* HLE: Refactoring of ApplicationLoader

* Fix SDL2 Headless

* Addresses gdkchan feedback

* Fixes LoadUnpackedNca RomFS loading

* remove useless casting

* Cleanup and fixe empty application name

* Remove ProcessInfo

* Fixes typo

* ActiveProcess to ActiveApplication

* Update check

* Clean using.

* Use the correct filepath when loading Homebrew.npdm

* Fix NRE in ProcessResult if MetaLoader is null

* Add more checks for valid processId & return success

* Add missing logging statement for npdm error

* Return result for LoadKip()

* Move error logging out of PFS load extension method

This avoids logging "Could not find Main NCA"
followed by "Loading main..." when trying to start hbl.

* Fix GUIs not checking load results

* Fix style and formatting issues

* Fix formatting and wording

* gtk: Refactor LoadApplication()

---------

Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
This commit is contained in:
Ac_K 2023-03-31 21:16:46 +02:00 committed by GitHub
parent 8198b99935
commit 4c2d9ff3ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1567 additions and 1322 deletions

View file

@ -320,10 +320,14 @@ namespace Ryujinx.Ava
_viewModel.IsGameRunning = true; _viewModel.IsGameRunning = true;
string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName) ? string.Empty : $" - {Device.Application.TitleName}"; var activeProcess = Device.Processes.ActiveApplication;
string titleVersionSection = string.IsNullOrWhiteSpace(Device.Application.DisplayVersion) ? string.Empty : $" v{Device.Application.DisplayVersion}"; var nacp = activeProcess.ApplicationControlProperties;
string titleIdSection = string.IsNullOrWhiteSpace(Device.Application.TitleIdText) ? string.Empty : $" ({Device.Application.TitleIdText.ToUpper()})"; int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
string titleArchSection = Device.Application.TitleIs64Bit ? " (64-bit)" : " (32-bit)";
string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}";
string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
Dispatcher.UIThread.InvokeAsync(() => Dispatcher.UIThread.InvokeAsync(() =>
{ {
@ -423,9 +427,9 @@ namespace Ryujinx.Ava
private void Dispose() private void Dispose()
{ {
if (Device.Application != null) if (Device.Processes != null)
{ {
_viewModel.UpdateGameMetadata(Device.Application.TitleIdText); _viewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText);
} }
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
@ -539,7 +543,12 @@ namespace Ryujinx.Ava
{ {
Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA)."); Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA).");
Device.LoadNca(ApplicationPath); if (!Device.LoadNca(ApplicationPath))
{
Device.Dispose();
return false;
}
} }
else if (Directory.Exists(ApplicationPath)) else if (Directory.Exists(ApplicationPath))
{ {
@ -554,13 +563,23 @@ namespace Ryujinx.Ava
{ {
Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS."); Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS.");
Device.LoadCart(ApplicationPath, romFsFiles[0]); if (!Device.LoadCart(ApplicationPath, romFsFiles[0]))
{
Device.Dispose();
return false;
}
} }
else else
{ {
Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
Device.LoadCart(ApplicationPath); if (!Device.LoadCart(ApplicationPath))
{
Device.Dispose();
return false;
}
} }
} }
else if (File.Exists(ApplicationPath)) else if (File.Exists(ApplicationPath))
@ -571,7 +590,12 @@ namespace Ryujinx.Ava
{ {
Logger.Info?.Print(LogClass.Application, "Loading as XCI."); Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
Device.LoadXci(ApplicationPath); if (!Device.LoadXci(ApplicationPath))
{
Device.Dispose();
return false;
}
break; break;
} }
@ -579,7 +603,12 @@ namespace Ryujinx.Ava
{ {
Logger.Info?.Print(LogClass.Application, "Loading as NCA."); Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
Device.LoadNca(ApplicationPath); if (!Device.LoadNca(ApplicationPath))
{
Device.Dispose();
return false;
}
break; break;
} }
@ -588,7 +617,12 @@ namespace Ryujinx.Ava
{ {
Logger.Info?.Print(LogClass.Application, "Loading as NSP."); Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
Device.LoadNsp(ApplicationPath); if (!Device.LoadNsp(ApplicationPath))
{
Device.Dispose();
return false;
}
break; break;
} }
@ -598,13 +632,18 @@ namespace Ryujinx.Ava
try try
{ {
Device.LoadProgram(ApplicationPath); if (!Device.LoadProgram(ApplicationPath))
{
Device.Dispose();
return false;
}
} }
catch (ArgumentOutOfRangeException) catch (ArgumentOutOfRangeException)
{ {
Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx."); Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
Dispose(); Device.Dispose();
return false; return false;
} }
@ -617,14 +656,14 @@ namespace Ryujinx.Ava
{ {
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/NRO file.");
Dispose(); Device.Dispose();
return false; return false;
} }
DiscordIntegrationModule.SwitchToPlayingState(Device.Application.TitleIdText, Device.Application.TitleName); DiscordIntegrationModule.SwitchToPlayingState(Device.Processes.ActiveApplication.ProgramIdText, Device.Processes.ActiveApplication.Name);
_viewModel.ApplicationLibrary.LoadAndSaveMetaData(Device.Application.TitleIdText, appMetadata => _viewModel.ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
{ {
appMetadata.LastPlayed = DateTime.UtcNow.ToString(); appMetadata.LastPlayed = DateTime.UtcNow.ToString();
}); });
@ -950,7 +989,7 @@ namespace Ryujinx.Ava
{ {
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen) if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
{ {
Device.Application.DiskCacheLoadState?.Cancel(); Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel();
} }
}); });
@ -1088,4 +1127,4 @@ namespace Ryujinx.Ava
return state; return state;
} }
} }
} }

View file

@ -19,6 +19,7 @@ using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using System; using System;
using System.Buffers; using System.Buffers;
@ -227,7 +228,7 @@ namespace Ryujinx.Ava.Common
return; return;
} }
(Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
if (updatePatchNca != null) if (updatePatchNca != null)
{ {
patchNca = updatePatchNca; patchNca = updatePatchNca;

View file

@ -1208,10 +1208,10 @@ namespace Ryujinx.Ava.UI.ViewModels
public void SetUIProgressHandlers(Switch emulationContext) public void SetUIProgressHandlers(Switch emulationContext)
{ {
if (emulationContext.Application.DiskCacheLoadState != null) if (emulationContext.Processes.ActiveApplication.DiskCacheLoadState != null)
{ {
emulationContext.Application.DiskCacheLoadState.StateChanged -= ProgressHandler; emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged -= ProgressHandler;
emulationContext.Application.DiskCacheLoadState.StateChanged += ProgressHandler; emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += ProgressHandler;
} }
emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler; emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler;
@ -1705,8 +1705,8 @@ namespace Ryujinx.Ava.UI.ViewModels
if (string.IsNullOrWhiteSpace(titleName)) if (string.IsNullOrWhiteSpace(titleName))
{ {
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Application.TitleName); LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
TitleName = AppHost.Device.Application.TitleName; TitleName = AppHost.Device.Processes.ActiveApplication.Name;
} }
SwitchToRenderer(startFullscreen); SwitchToRenderer(startFullscreen);

View file

@ -17,6 +17,7 @@ using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.Ui.App.Common;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -162,7 +163,7 @@ public class TitleUpdateViewModel : BaseModel
try try
{ {
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0); (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
if (controlNca != null && patchNca != null) if (controlNca != null && patchNca != null)
{ {

View file

@ -126,7 +126,7 @@ namespace Ryujinx.Ava.UI.Views.Main
if (ViewModel.AppHost.Device.System.SearchingForAmiibo(out int deviceId)) if (ViewModel.AppHost.Device.System.SearchingForAmiibo(out int deviceId))
{ {
string titleId = ViewModel.AppHost.Device.Application.TitleIdText.ToUpper(); string titleId = ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper();
AmiiboWindow window = new(ViewModel.ShowAll, ViewModel.LastScannedAmiiboId, titleId); AmiiboWindow window = new(ViewModel.ShowAll, ViewModel.LastScannedAmiiboId, titleId);
await window.ShowDialog(Window); await window.ShowDialog(Window);
@ -148,13 +148,11 @@ namespace Ryujinx.Ava.UI.Views.Main
return; return;
} }
ApplicationLoader application = ViewModel.AppHost.Device.Application; string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString();
if (application != null)
{
await new CheatWindow(Window.VirtualFileSystem, application.TitleIdText, application.TitleName).ShowDialog(Window);
ViewModel.AppHost.Device.EnableCheats(); await new CheatWindow(Window.VirtualFileSystem, ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText, name).ShowDialog(Window);
}
ViewModel.AppHost.Device.EnableCheats();
} }
private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)

View file

@ -20,7 +20,6 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Path = System.IO.Path; using Path = System.IO.Path;
using RightsId = LibHac.Fs.RightsId; using RightsId = LibHac.Fs.RightsId;
@ -146,6 +145,7 @@ namespace Ryujinx.HLE.FileSystem
return $"{basePath}:/{fileName}"; return $"{basePath}:/{fileName}";
} }
return null; return null;
} }
@ -191,7 +191,7 @@ namespace Ryujinx.HLE.FileSystem
fsServerClient = horizon.CreatePrivilegedHorizonClient(); fsServerClient = horizon.CreatePrivilegedHorizonClient();
var fsServer = new FileSystemServer(fsServerClient); var fsServer = new FileSystemServer(fsServerClient);
RandomDataGenerator randomGenerator = buffer => Random.Shared.NextBytes(buffer); RandomDataGenerator randomGenerator = Random.Shared.NextBytes;
DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet, fsServer, randomGenerator); DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet, fsServer, randomGenerator);
@ -264,7 +264,7 @@ namespace Ryujinx.HLE.FileSystem
if (result.IsSuccess()) if (result.IsSuccess())
{ {
Ticket ticket = new Ticket(ticketFile.Get.AsStream()); Ticket ticket = new(ticketFile.Get.AsStream());
var titleKey = ticket.GetTitleKey(KeySet); var titleKey = ticket.GetTitleKey(KeySet);
if (titleKey != null) if (titleKey != null)

View file

@ -1,908 +0,0 @@
using LibHac;
using LibHac.Account;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Fs.Shim;
using LibHac.FsSystem;
using LibHac.Loader;
using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using static Ryujinx.HLE.HOS.ModLoader;
using ApplicationId = LibHac.Ncm.ApplicationId;
using Path = System.IO.Path;
namespace Ryujinx.HLE.HOS
{
using JsonHelper = Common.Utilities.JsonHelper;
public class ApplicationLoader
{
// Binaries from exefs are loaded into mem in this order. Do not change.
internal static readonly string[] ExeFsPrefixes =
{
"rtld",
"main",
"subsdk0",
"subsdk1",
"subsdk2",
"subsdk3",
"subsdk4",
"subsdk5",
"subsdk6",
"subsdk7",
"subsdk8",
"subsdk9",
"sdk"
};
private readonly Switch _device;
private string _titleName;
private string _displayVersion;
private BlitStruct<ApplicationControlProperty> _controlData;
public BlitStruct<ApplicationControlProperty> ControlData => _controlData;
public string TitleName => _titleName;
public string DisplayVersion => _displayVersion;
public ulong TitleId { get; private set; }
public bool TitleIs64Bit { get; private set; }
public string TitleIdText => TitleId.ToString("x16");
public IDiskCacheLoadState DiskCacheLoadState { get; private set; }
public ApplicationLoader(Switch device)
{
_device = device;
_controlData = new BlitStruct<ApplicationControlProperty>(1);
}
public void LoadCart(string exeFsDir, string romFsFile = null)
{
LocalFileSystem codeFs = new LocalFileSystem(exeFsDir);
MetaLoader metaData = ReadNpdm(codeFs);
_device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
new[] { TitleId },
_device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(),
_device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath());
if (TitleId != 0)
{
EnsureSaveData(new ApplicationId(TitleId));
}
ulong pid = LoadExeFs(codeFs, string.Empty, metaData);
if (romFsFile != null)
{
_device.Configuration.VirtualFileSystem.LoadRomFs(pid, romFsFile);
}
}
public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex)
{
Nca mainNca = null;
Nca patchNca = null;
Nca controlNca = null;
fileSystem.ImportTickets(pfs);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(fileSystem.KeySet, ncaFile.Release().AsStorage());
int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
if (ncaProgramIndex != programIndex)
{
continue;
}
if (nca.Header.ContentType == NcaContentType.Program)
{
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{
patchNca = nca;
}
else
{
mainNca = nca;
}
}
else if (nca.Header.ContentType == NcaContentType.Control)
{
controlNca = nca;
}
}
return (mainNca, patchNca, controlNca);
}
public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex)
{
Nca patchNca = null;
Nca controlNca = null;
fileSystem.ImportTickets(pfs);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(fileSystem.KeySet, ncaFile.Release().AsStorage());
int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
if (ncaProgramIndex != programIndex)
{
continue;
}
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId)
{
break;
}
if (nca.Header.ContentType == NcaContentType.Program)
{
patchNca = nca;
}
else if (nca.Header.ContentType == NcaContentType.Control)
{
controlNca = nca;
}
}
return (patchNca, controlNca);
}
public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath)
{
updatePath = null;
if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
{
// Clear the program index part.
titleIdBase &= 0xFFFFFFFFFFFFFFF0;
// Load update informations if existing.
string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
if (File.Exists(titleUpdateMetadataPath))
{
updatePath = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(titleUpdateMetadataPath).Selected;
if (File.Exists(updatePath))
{
FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex);
}
}
}
return (null, null);
}
public void LoadXci(string xciFile)
{
FileStream file = new FileStream(xciFile, FileMode.Open, FileAccess.Read);
Xci xci = new Xci(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage());
if (!xci.HasPartition(XciPartitionType.Secure))
{
Logger.Error?.Print(LogClass.Loader, "Unable to load XCI: Could not find XCI secure partition");
return;
}
PartitionFileSystem securePartition = xci.OpenPartition(XciPartitionType.Secure);
Nca mainNca;
Nca patchNca;
Nca controlNca;
try
{
(mainNca, patchNca, controlNca) = GetGameData(_device.Configuration.VirtualFileSystem, securePartition, _device.Configuration.UserChannelPersistence.Index);
RegisterProgramMapInfo(securePartition).ThrowIfFailure();
}
catch (Exception e)
{
Logger.Error?.Print(LogClass.Loader, $"Unable to load XCI: {e.Message}");
return;
}
if (mainNca == null)
{
Logger.Error?.Print(LogClass.Loader, "Unable to load XCI: Could not find Main NCA");
return;
}
_device.Configuration.ContentManager.LoadEntries(_device);
_device.Configuration.ContentManager.ClearAocData();
_device.Configuration.ContentManager.AddAocData(securePartition, xciFile, mainNca.Header.TitleId, _device.Configuration.FsIntegrityCheckLevel);
LoadNca(mainNca, patchNca, controlNca);
}
public void LoadNsp(string nspFile)
{
FileStream file = new FileStream(nspFile, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
Nca mainNca;
Nca patchNca;
Nca controlNca;
try
{
(mainNca, patchNca, controlNca) = GetGameData(_device.Configuration.VirtualFileSystem, nsp, _device.Configuration.UserChannelPersistence.Index);
RegisterProgramMapInfo(nsp).ThrowIfFailure();
}
catch (Exception e)
{
Logger.Error?.Print(LogClass.Loader, $"Unable to load NSP: {e.Message}");
return;
}
if (mainNca != null)
{
_device.Configuration.ContentManager.ClearAocData();
_device.Configuration.ContentManager.AddAocData(nsp, nspFile, mainNca.Header.TitleId, _device.Configuration.FsIntegrityCheckLevel);
LoadNca(mainNca, patchNca, controlNca);
return;
}
// This is not a normal NSP, it's actually a ExeFS as a NSP
LoadExeFs(nsp, null, isHomebrew: true);
}
public void LoadNca(string ncaFile)
{
FileStream file = new FileStream(ncaFile, FileMode.Open, FileAccess.Read);
Nca nca = new Nca(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage(false));
LoadNca(nca, null, null);
}
public void LoadServiceNca(string ncaFile)
{
FileStream file = new FileStream(ncaFile, FileMode.Open, FileAccess.Read);
Nca mainNca = new Nca(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage(false));
if (mainNca.Header.ContentType != NcaContentType.Program)
{
Logger.Error?.Print(LogClass.Loader, "Selected NCA is not a \"Program\" NCA");
return;
}
IFileSystem codeFs = null;
if (mainNca.CanOpenSection(NcaSectionType.Code))
{
codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, _device.System.FsIntegrityCheckLevel);
}
if (codeFs == null)
{
Logger.Error?.Print(LogClass.Loader, "No ExeFS found in NCA");
return;
}
using var npdmFile = new UniqueRef<IFile>();
Result result = codeFs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
MetaLoader metaData;
npdmFile.Get.GetSize(out long fileSize).ThrowIfFailure();
var npdmBuffer = new byte[fileSize];
npdmFile.Get.Read(out _, 0, npdmBuffer).ThrowIfFailure();
metaData = new MetaLoader();
metaData.Load(npdmBuffer).ThrowIfFailure();
NsoExecutable[] nsos = new NsoExecutable[ExeFsPrefixes.Length];
for (int i = 0; i < nsos.Length; i++)
{
string name = ExeFsPrefixes[i];
if (!codeFs.FileExists($"/{name}"))
{
continue; // File doesn't exist, skip.
}
Logger.Info?.Print(LogClass.Loader, $"Loading {name}...");
using var nsoFile = new UniqueRef<IFile>();
codeFs.OpenFile(ref nsoFile.Ref, $"/{name}".ToU8Span(), OpenMode.Read).ThrowIfFailure();
nsos[i] = new NsoExecutable(nsoFile.Release().AsStorage(), name);
}
// Collect the nsos, ignoring ones that aren't used.
NsoExecutable[] programs = nsos.Where(x => x != null).ToArray();
string displayVersion = _device.System.ContentManager.GetCurrentFirmwareVersion().VersionString;
bool usePtc = _device.System.EnablePtc;
metaData.GetNpdm(out Npdm npdm).ThrowIfFailure();
ProgramInfo programInfo = new ProgramInfo(in npdm, displayVersion, usePtc, allowCodeMemoryForJit: false);
ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, programInfo, executables: programs);
string titleIdText = npdm.Aci.ProgramId.Value.ToString("x16");
bool titleIs64Bit = (npdm.Meta.Flags & 1) != 0;
string programName = Encoding.ASCII.GetString(npdm.Meta.ProgramName).TrimEnd('\0');
Logger.Info?.Print(LogClass.Loader, $"Service Loaded: {programName} [{titleIdText}] [{(titleIs64Bit ? "64-bit" : "32-bit")}]");
}
private void LoadNca(Nca mainNca, Nca patchNca, Nca controlNca)
{
if (mainNca.Header.ContentType != NcaContentType.Program)
{
Logger.Error?.Print(LogClass.Loader, "Selected NCA is not a \"Program\" NCA");
return;
}
IStorage dataStorage = null;
IFileSystem codeFs = null;
(Nca updatePatchNca, Nca updateControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), _device.Configuration.UserChannelPersistence.Index, out _);
if (updatePatchNca != null)
{
patchNca = updatePatchNca;
}
if (updateControlNca != null)
{
controlNca = updateControlNca;
}
// Load program 0 control NCA as we are going to need it for display version.
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
// Load Aoc
string titleAocMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json");
if (File.Exists(titleAocMetadataPath))
{
List<DownloadableContentContainer> dlcContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(titleAocMetadataPath);
foreach (DownloadableContentContainer downloadableContentContainer in dlcContainerList)
{
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
{
if (File.Exists(downloadableContentContainer.ContainerPath) && downloadableContentNca.Enabled)
{
_device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath);
}
else
{
Logger.Warning?.Print(LogClass.Application, $"Cannot find AddOnContent file {downloadableContentContainer.ContainerPath}. It may have been moved or renamed.");
}
}
}
}
if (patchNca == null)
{
if (mainNca.CanOpenSection(NcaSectionType.Data))
{
dataStorage = mainNca.OpenStorage(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
}
if (mainNca.CanOpenSection(NcaSectionType.Code))
{
codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, _device.System.FsIntegrityCheckLevel);
}
}
else
{
if (patchNca.CanOpenSection(NcaSectionType.Data))
{
dataStorage = mainNca.OpenStorageWithPatch(patchNca, NcaSectionType.Data, _device.System.FsIntegrityCheckLevel);
}
if (patchNca.CanOpenSection(NcaSectionType.Code))
{
codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, _device.System.FsIntegrityCheckLevel);
}
}
if (codeFs == null)
{
Logger.Error?.Print(LogClass.Loader, "No ExeFS found in NCA");
return;
}
MetaLoader metaData = ReadNpdm(codeFs);
_device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
_device.Configuration.ContentManager.GetAocTitleIds().Prepend(TitleId),
_device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(),
_device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath());
string displayVersion = string.Empty;
if (controlNca != null)
{
ReadControlData(_device, controlNca, ref _controlData, ref _titleName, ref displayVersion);
}
else
{
ControlData.ByteSpan.Clear();
}
// NOTE: Nintendo doesn't guarantee that the display version will be updated on sub programs when updating a multi program application.
// BODY: As such, to avoid PTC cache confusion, we only trust the the program 0 display version when launching a sub program.
if (updateProgram0ControlNca != null && _device.Configuration.UserChannelPersistence.Index != 0)
{
string dummyTitleName = "";
BlitStruct<ApplicationControlProperty> dummyControl = new BlitStruct<ApplicationControlProperty>(1);
ReadControlData(_device, updateProgram0ControlNca, ref dummyControl, ref dummyTitleName, ref displayVersion);
}
_displayVersion = displayVersion;
ulong pid = LoadExeFs(codeFs, displayVersion, metaData);
if (dataStorage == null)
{
Logger.Warning?.Print(LogClass.Loader, "No RomFS found in NCA");
}
else
{
IStorage newStorage = _device.Configuration.VirtualFileSystem.ModLoader.ApplyRomFsMods(TitleId, dataStorage);
_device.Configuration.VirtualFileSystem.SetRomFs(pid, newStorage.AsStream(FileAccess.Read));
}
// Don't create save data for system programs.
if (TitleId != 0 && (TitleId < SystemProgramId.Start.Value || TitleId > SystemAppletId.End.Value))
{
// Multi-program applications can technically use any program ID for the main program, but in practice they always use 0 in the low nibble.
// We'll know if this changes in the future because stuff will get errors when trying to mount the correct save.
EnsureSaveData(new ApplicationId(TitleId & ~0xFul));
}
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {TitleName} v{DisplayVersion} [{TitleIdText}] [{(TitleIs64Bit ? "64-bit" : "32-bit")}]");
}
// Sets TitleId, so be sure to call before using it
private MetaLoader ReadNpdm(IFileSystem fs)
{
using var npdmFile = new UniqueRef<IFile>();
Result result = fs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
MetaLoader metaData;
if (ResultFs.PathNotFound.Includes(result))
{
Logger.Warning?.Print(LogClass.Loader, "NPDM file not found, using default values!");
metaData = GetDefaultNpdm();
}
else
{
npdmFile.Get.GetSize(out long fileSize).ThrowIfFailure();
var npdmBuffer = new byte[fileSize];
npdmFile.Get.Read(out _, 0, npdmBuffer).ThrowIfFailure();
metaData = new MetaLoader();
metaData.Load(npdmBuffer).ThrowIfFailure();
}
metaData.GetNpdm(out var npdm).ThrowIfFailure();
TitleId = npdm.Aci.ProgramId.Value;
TitleIs64Bit = (npdm.Meta.Flags & 1) != 0;
_device.System.LibHacHorizonManager.ArpIReader.ApplicationId = new LibHac.ApplicationId(TitleId);
return metaData;
}
private static void ReadControlData(Switch device, Nca controlNca, ref BlitStruct<ApplicationControlProperty> controlData, ref string titleName, ref string displayVersion)
{
using var controlFile = new UniqueRef<IFile>();
IFileSystem controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, device.System.FsIntegrityCheckLevel);
Result result = controlFs.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
result = controlFile.Get.Read(out long bytesRead, 0, controlData.ByteSpan, ReadOption.None);
if (result.IsSuccess() && bytesRead == controlData.ByteSpan.Length)
{
titleName = controlData.Value.Title[(int)device.System.State.DesiredTitleLanguage].NameString.ToString();
if (string.IsNullOrWhiteSpace(titleName))
{
titleName = controlData.Value.Title.ItemsRo.ToArray().FirstOrDefault(x => x.Name[0] != 0).NameString.ToString();
}
displayVersion = controlData.Value.DisplayVersionString.ToString();
}
}
else
{
controlData.ByteSpan.Clear();
}
}
private ulong LoadExeFs(IFileSystem codeFs, string displayVersion, MetaLoader metaData = null, bool isHomebrew = false)
{
if (_device.Configuration.VirtualFileSystem.ModLoader.ReplaceExefsPartition(TitleId, ref codeFs))
{
metaData = null; // TODO: Check if we should retain old npdm.
}
metaData ??= ReadNpdm(codeFs);
NsoExecutable[] nsos = new NsoExecutable[ExeFsPrefixes.Length];
for (int i = 0; i < nsos.Length; i++)
{
string name = ExeFsPrefixes[i];
if (!codeFs.FileExists($"/{name}"))
{
continue; // File doesn't exist, skip.
}
Logger.Info?.Print(LogClass.Loader, $"Loading {name}...");
using var nsoFile = new UniqueRef<IFile>();
codeFs.OpenFile(ref nsoFile.Ref, $"/{name}".ToU8Span(), OpenMode.Read).ThrowIfFailure();
nsos[i] = new NsoExecutable(nsoFile.Release().AsStorage(), name);
}
// ExeFs file replacements.
ModLoadResult modLoadResult = _device.Configuration.VirtualFileSystem.ModLoader.ApplyExefsMods(TitleId, nsos);
// Collect the nsos, ignoring ones that aren't used.
NsoExecutable[] programs = nsos.Where(x => x != null).ToArray();
// Take the npdm from mods if present.
if (modLoadResult.Npdm != null)
{
metaData = modLoadResult.Npdm;
}
_device.Configuration.VirtualFileSystem.ModLoader.ApplyNsoPatches(TitleId, programs);
_device.Configuration.ContentManager.LoadEntries(_device);
bool usePtc = _device.System.EnablePtc;
// Don't use PPTC if ExeFs files have been replaced.
usePtc &= !modLoadResult.Modified;
if (_device.System.EnablePtc && !usePtc)
{
Logger.Warning?.Print(LogClass.Ptc, $"Detected unsupported ExeFs modifications. PPTC disabled.");
}
Graphics.Gpu.GraphicsConfig.TitleId = TitleIdText;
_device.Gpu.HostInitalized.Set();
MemoryManagerMode memoryManagerMode = _device.Configuration.MemoryManagerMode;
if (!MemoryBlock.SupportsFlags(MemoryAllocationFlags.ViewCompatible))
{
memoryManagerMode = MemoryManagerMode.SoftwarePageTable;
}
// We allow it for nx-hbloader because it can be used to launch homebrew.
bool allowCodeMemoryForJit = TitleId == 0x010000000000100DUL || isHomebrew;
metaData.GetNpdm(out Npdm npdm).ThrowIfFailure();
ProgramInfo programInfo = new ProgramInfo(in npdm, displayVersion, usePtc, allowCodeMemoryForJit);
ProgramLoadResult result = ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, programInfo, executables: programs);
DiskCacheLoadState = result.DiskCacheLoadState;
_device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(TitleId, result.TamperInfo, _device.TamperMachine);
return result.ProcessId;
}
public void LoadProgram(string filePath)
{
MetaLoader metaData = GetDefaultNpdm();
metaData.GetNpdm(out Npdm npdm).ThrowIfFailure();
ProgramInfo programInfo = new ProgramInfo(in npdm, string.Empty, diskCacheEnabled: false, allowCodeMemoryForJit: true);
bool isNro = Path.GetExtension(filePath).ToLower() == ".nro";
IExecutable executable;
Stream romfsStream = null;
if (isNro)
{
FileStream input = new FileStream(filePath, FileMode.Open);
NroExecutable obj = new NroExecutable(input.AsStorage());
executable = obj;
// Homebrew NRO can actually have some data after the actual NRO.
if (input.Length > obj.FileSize)
{
input.Position = obj.FileSize;
BinaryReader reader = new BinaryReader(input);
uint asetMagic = reader.ReadUInt32();
if (asetMagic == 0x54455341)
{
uint asetVersion = reader.ReadUInt32();
if (asetVersion == 0)
{
ulong iconOffset = reader.ReadUInt64();
ulong iconSize = reader.ReadUInt64();
ulong nacpOffset = reader.ReadUInt64();
ulong nacpSize = reader.ReadUInt64();
ulong romfsOffset = reader.ReadUInt64();
ulong romfsSize = reader.ReadUInt64();
if (romfsSize != 0)
{
romfsStream = new HomebrewRomFsStream(input, obj.FileSize + (long)romfsOffset);
}
if (nacpSize != 0)
{
input.Seek(obj.FileSize + (long)nacpOffset, SeekOrigin.Begin);
reader.Read(ControlData.ByteSpan);
ref ApplicationControlProperty nacp = ref ControlData.Value;
programInfo.Name = nacp.Title[(int)_device.System.State.DesiredTitleLanguage].NameString.ToString();
if (string.IsNullOrWhiteSpace(programInfo.Name))
{
programInfo.Name = nacp.Title.ItemsRo.ToArray().FirstOrDefault(x => x.Name[0] != 0).NameString.ToString();
}
if (nacp.PresenceGroupId != 0)
{
programInfo.ProgramId = nacp.PresenceGroupId;
}
else if (nacp.SaveDataOwnerId != 0)
{
programInfo.ProgramId = nacp.SaveDataOwnerId;
}
else if (nacp.AddOnContentBaseId != 0)
{
programInfo.ProgramId = nacp.AddOnContentBaseId - 0x1000;
}
else
{
programInfo.ProgramId = 0000000000000000;
}
}
}
else
{
Logger.Warning?.Print(LogClass.Loader, $"Unsupported ASET header version found \"{asetVersion}\"");
}
}
}
}
else
{
executable = new NsoExecutable(new LocalStorage(filePath, FileAccess.Read), Path.GetFileNameWithoutExtension(filePath));
}
_device.Configuration.ContentManager.LoadEntries(_device);
_titleName = programInfo.Name;
TitleId = programInfo.ProgramId;
TitleIs64Bit = (npdm.Meta.Flags & 1) != 0;
_device.System.LibHacHorizonManager.ArpIReader.ApplicationId = new LibHac.ApplicationId(TitleId);
// Explicitly null titleid to disable the shader cache.
Graphics.Gpu.GraphicsConfig.TitleId = null;
_device.Gpu.HostInitalized.Set();
ProgramLoadResult result = ProgramLoader.LoadNsos(_device.System.KernelContext, metaData, programInfo, executables: executable);
if (romfsStream != null)
{
_device.Configuration.VirtualFileSystem.SetRomFs(result.ProcessId, romfsStream);
}
DiskCacheLoadState = result.DiskCacheLoadState;
_device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(TitleId, result.TamperInfo, _device.TamperMachine);
}
private MetaLoader GetDefaultNpdm()
{
Assembly asm = Assembly.GetCallingAssembly();
using (Stream npdmStream = asm.GetManifestResourceStream("Ryujinx.HLE.Homebrew.npdm"))
{
var npdmBuffer = new byte[npdmStream.Length];
npdmStream.Read(npdmBuffer);
var metaLoader = new MetaLoader();
metaLoader.Load(npdmBuffer).ThrowIfFailure();
return metaLoader;
}
}
private static (ulong applicationId, int programCount) GetMultiProgramInfo(VirtualFileSystem fileSystem, PartitionFileSystem pfs)
{
ulong mainProgramId = 0;
Span<bool> hasIndex = stackalloc bool[0x10];
fileSystem.ImportTickets(pfs);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(fileSystem.KeySet, ncaFile.Release().AsStorage());
if (nca.Header.ContentType != NcaContentType.Program)
{
continue;
}
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{
continue;
}
ulong currentProgramId = nca.Header.TitleId;
ulong currentMainProgramId = currentProgramId & ~0xFFFul;
if (mainProgramId == 0 && currentMainProgramId != 0)
{
mainProgramId = currentMainProgramId;
}
if (mainProgramId != currentMainProgramId)
{
// As far as I know there aren't any multi-application game cards containing multi-program applications,
// so because multi-application game cards are the only way we should run into multiple applications
// we'll just return that there's a single program.
return (mainProgramId, 1);
}
hasIndex[(int)(currentProgramId & 0xF)] = true;
}
int programCount = 0;
for (int i = 0; i < hasIndex.Length && hasIndex[i]; i++)
{
programCount++;
}
return (mainProgramId, programCount);
}
private Result RegisterProgramMapInfo(PartitionFileSystem pfs)
{
(ulong applicationId, int programCount) = GetMultiProgramInfo(_device.Configuration.VirtualFileSystem, pfs);
if (programCount <= 0)
return Result.Success;
Span<ProgramIndexMapInfo> mapInfo = stackalloc ProgramIndexMapInfo[0x10];
for (int i = 0; i < programCount; i++)
{
mapInfo[i].ProgramId = new ProgramId(applicationId + (uint)i);
mapInfo[i].MainProgramId = new ApplicationId(applicationId);
mapInfo[i].ProgramIndex = (byte)i;
}
return _device.System.LibHacHorizonManager.NsClient.Fs.RegisterProgramIndexMapInfo(mapInfo.Slice(0, programCount));
}
private Result EnsureSaveData(ApplicationId applicationId)
{
Logger.Info?.Print(LogClass.Application, "Ensuring required savedata exists.");
Uid user = _device.System.AccountManager.LastOpenedUser.UserId.ToLibHacUid();
ref ApplicationControlProperty control = ref ControlData.Value;
if (LibHac.Common.Utilities.IsZeros(ControlData.ByteSpan))
{
// If the current application doesn't have a loaded control property, create a dummy one
// and set the savedata sizes so a user savedata will be created.
control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
// The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
control.UserAccountSaveDataSize = 0x4000;
control.UserAccountSaveDataJournalSize = 0x4000;
control.SaveDataOwnerId = applicationId.Value;
Logger.Warning?.Print(LogClass.Application,
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
}
HorizonClient hos = _device.System.LibHacHorizonManager.RyujinxClient;
Result resultCode = hos.Fs.EnsureApplicationCacheStorage(out _, out _, applicationId, in control);
if (resultCode.IsFailure())
{
Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {resultCode.ToStringWithName()}");
return resultCode;
}
resultCode = hos.Fs.EnsureApplicationSaveData(out _, applicationId, in control, in user);
if (resultCode.IsFailure())
{
Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {resultCode.ToStringWithName()}");
}
return resultCode;
}
}
}

View file

@ -35,6 +35,7 @@ using Ryujinx.HLE.HOS.Services.SurfaceFlinger;
using Ryujinx.HLE.HOS.Services.Time.Clock; using Ryujinx.HLE.HOS.Services.Time.Clock;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon; using Ryujinx.Horizon;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -358,11 +359,11 @@ namespace Ryujinx.HLE.HOS
} }
} }
public void LoadKip(string kipPath) public bool LoadKip(string kipPath)
{ {
using var kipFile = new SharedRef<IStorage>(new LocalStorage(kipPath, FileAccess.Read)); using var kipFile = new SharedRef<IStorage>(new LocalStorage(kipPath, FileAccess.Read));
ProgramLoader.LoadKip(KernelContext, new KipExecutable(in kipFile)); return ProcessLoaderHelper.LoadKip(KernelContext, new KipExecutable(in kipFile));
} }
public void ChangeDockedModeState(bool newState) public void ChangeDockedModeState(bool newState)

View file

@ -2,7 +2,7 @@ using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Kernel.Process namespace Ryujinx.HLE.HOS.Kernel.Process
{ {
internal class ProcessTamperInfo class ProcessTamperInfo
{ {
public KProcess Process { get; } public KProcess Process { get; }
public IEnumerable<string> BuildIds { get; } public IEnumerable<string> BuildIds { get; }

View file

@ -10,6 +10,7 @@ using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Mods; using Ryujinx.HLE.Loaders.Mods;
using Ryujinx.HLE.Loaders.Processes;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
@ -547,7 +548,7 @@ namespace Ryujinx.HLE.HOS
return modLoadResult; return modLoadResult;
} }
if (nsos.Length != ApplicationLoader.ExeFsPrefixes.Length) if (nsos.Length != ProcessConst.ExeFsPrefixes.Length)
{ {
throw new ArgumentOutOfRangeException("NSO Count is incorrect"); throw new ArgumentOutOfRangeException("NSO Count is incorrect");
} }
@ -556,9 +557,9 @@ namespace Ryujinx.HLE.HOS
foreach (var mod in exeMods) foreach (var mod in exeMods)
{ {
for (int i = 0; i < ApplicationLoader.ExeFsPrefixes.Length; ++i) for (int i = 0; i < ProcessConst.ExeFsPrefixes.Length; ++i)
{ {
var nsoName = ApplicationLoader.ExeFsPrefixes[i]; var nsoName = ProcessConst.ExeFsPrefixes[i];
FileInfo nsoFile = new FileInfo(Path.Combine(mod.Path.FullName, nsoName)); FileInfo nsoFile = new FileInfo(Path.Combine(mod.Path.FullName, nsoName));
if (nsoFile.Exists) if (nsoFile.Exists)
@ -596,7 +597,7 @@ namespace Ryujinx.HLE.HOS
} }
} }
for (int i = ApplicationLoader.ExeFsPrefixes.Length - 1; i >= 0; --i) for (int i = ProcessConst.ExeFsPrefixes.Length - 1; i >= 0; --i)
{ {
if (modLoadResult.Stubs[1 << i] && !modLoadResult.Replaces[1 << i]) // Prioritizes replacements over stubs if (modLoadResult.Stubs[1 << i] && !modLoadResult.Replaces[1 << i]) // Prioritizes replacements over stubs
{ {

View file

@ -190,7 +190,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
// TODO: Account actually calls nn::arp::detail::IReader::GetApplicationControlProperty() with the current Pid and store the result (NACP file) internally. // TODO: Account actually calls nn::arp::detail::IReader::GetApplicationControlProperty() with the current Pid and store the result (NACP file) internally.
// But since we use LibHac and we load one Application at a time, it's not necessary. // But since we use LibHac and we load one Application at a time, it's not necessary.
context.ResponseData.Write((byte)context.Device.Application.ControlData.Value.UserAccountSwitchLock); context.ResponseData.Write((byte)context.Device.Processes.ActiveApplication.ApplicationControlProperties.UserAccountSwitchLock);
Logger.Stub?.PrintStub(LogClass.ServiceAcc); Logger.Stub?.PrintStub(LogClass.ServiceAcc);

View file

@ -9,7 +9,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
public ILibraryAppletSelfAccessor(ServiceCtx context) public ILibraryAppletSelfAccessor(ServiceCtx context)
{ {
if (context.Device.Application.TitleId == 0x0100000000001009) if (context.Device.Processes.ActiveApplication.ProgramId == 0x0100000000001009)
{ {
// Create MiiEdit data. // Create MiiEdit data.
_appletStandalone = new AppletStandalone() _appletStandalone = new AppletStandalone()
@ -25,7 +25,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
} }
else else
{ {
throw new NotImplementedException($"{context.Device.Application.TitleId} applet is not implemented."); throw new NotImplementedException($"{context.Device.Processes.ActiveApplication.ProgramId} applet is not implemented.");
} }
} }

View file

@ -115,28 +115,12 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid(); Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid();
// Mask out the low nibble of the program ID to get the application ID // Mask out the low nibble of the program ID to get the application ID
ApplicationId applicationId = new ApplicationId(context.Device.Application.TitleId & ~0xFul); ApplicationId applicationId = new ApplicationId(context.Device.Processes.ActiveApplication.ProgramId & ~0xFul);
BlitStruct<ApplicationControlProperty> controlHolder = context.Device.Application.ControlData; ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
ref ApplicationControlProperty control = ref controlHolder.Value;
if (LibHac.Common.Utilities.IsZeros(controlHolder.ByteSpan))
{
// If the current application doesn't have a loaded control property, create a dummy one
// and set the savedata sizes so a user savedata will be created.
control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
// The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
control.UserAccountSaveDataSize = 0x4000;
control.UserAccountSaveDataJournalSize = 0x4000;
Logger.Warning?.Print(LogClass.ServiceAm,
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
}
LibHac.HorizonClient hos = context.Device.System.LibHacHorizonManager.AmClient; LibHac.HorizonClient hos = context.Device.System.LibHacHorizonManager.AmClient;
LibHac.Result result = hos.Fs.EnsureApplicationSaveData(out long requiredSize, applicationId, in control, in userId); LibHac.Result result = hos.Fs.EnsureApplicationSaveData(out long requiredSize, applicationId, in nacp, in userId);
context.ResponseData.Write(requiredSize); context.ResponseData.Write(requiredSize);
@ -153,7 +137,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
// TODO: When above calls are implemented, switch to using ns:am // TODO: When above calls are implemented, switch to using ns:am
long desiredLanguageCode = context.Device.System.State.DesiredLanguageCode; long desiredLanguageCode = context.Device.System.State.DesiredLanguageCode;
int supportedLanguages = (int)context.Device.Application.ControlData.Value.SupportedLanguageFlag; int supportedLanguages = (int)context.Device.Processes.ActiveApplication.ApplicationControlProperties.SupportedLanguageFlag;
int firstSupported = BitOperations.TrailingZeroCount(supportedLanguages); int firstSupported = BitOperations.TrailingZeroCount(supportedLanguages);
if (firstSupported > (int)TitleLanguage.BrazilianPortuguese) if (firstSupported > (int)TitleLanguage.BrazilianPortuguese)
@ -196,7 +180,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
public ResultCode GetDisplayVersion(ServiceCtx context) public ResultCode GetDisplayVersion(ServiceCtx context)
{ {
// If an NACP isn't found, the buffer will be all '\0' which seems to be the correct implementation. // If an NACP isn't found, the buffer will be all '\0' which seems to be the correct implementation.
context.ResponseData.Write(context.Device.Application.ControlData.Value.DisplayVersion); context.ResponseData.Write(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion);
return ResultCode.Success; return ResultCode.Success;
} }
@ -251,13 +235,12 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
long journalSize = context.RequestData.ReadInt64(); long journalSize = context.RequestData.ReadInt64();
// Mask out the low nibble of the program ID to get the application ID // Mask out the low nibble of the program ID to get the application ID
ApplicationId applicationId = new ApplicationId(context.Device.Application.TitleId & ~0xFul); ApplicationId applicationId = new ApplicationId(context.Device.Processes.ActiveApplication.ProgramId & ~0xFul);
BlitStruct<ApplicationControlProperty> controlHolder = context.Device.Application.ControlData; ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
LibHac.Result result = _horizon.Fs.CreateApplicationCacheStorage(out long requiredSize, LibHac.Result result = _horizon.Fs.CreateApplicationCacheStorage(out long requiredSize,
out CacheStorageTargetMedia storageTarget, applicationId, in controlHolder.Value, index, saveSize, out CacheStorageTargetMedia storageTarget, applicationId, in nacp, index, saveSize, journalSize);
journalSize);
if (result.IsFailure()) return (ResultCode)result.Value; if (result.IsFailure()) return (ResultCode)result.Value;
@ -677,7 +660,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
throw new InvalidSystemResourceException($"JIT (010000000000003B) system title not found! The JIT will not work, provide the system archive to fix this error. (See https://github.com/Ryujinx/Ryujinx#requirements for more information)"); throw new InvalidSystemResourceException($"JIT (010000000000003B) system title not found! The JIT will not work, provide the system archive to fix this error. (See https://github.com/Ryujinx/Ryujinx#requirements for more information)");
} }
context.Device.Application.LoadServiceNca(filePath); context.Device.LoadNca(filePath);
// FIXME: Most likely not how this should be done? // FIXME: Most likely not how this should be done?
while (!context.Device.System.SmRegistry.IsServiceRegistered("jit:u")) while (!context.Device.System.SmRegistry.IsServiceRegistered("jit:u"))

View file

@ -33,7 +33,7 @@ namespace Ryujinx.HLE.HOS.Services.Arp
return new ApplicationLaunchProperty return new ApplicationLaunchProperty
{ {
TitleId = context.Device.Application.TitleId, TitleId = context.Device.Processes.ActiveApplication.ProgramId,
Version = 0x00, Version = 0x00,
BaseGameStorageId = (byte)StorageId.BuiltInSystem, BaseGameStorageId = (byte)StorageId.BuiltInSystem,
UpdateGameStorageId = (byte)StorageId.None UpdateGameStorageId = (byte)StorageId.None

View file

@ -31,7 +31,7 @@ namespace Ryujinx.HLE.HOS.Services.Caps
byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray(); byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray();
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Application.TitleId, out ApplicationAlbumEntry applicationAlbumEntry); ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
context.ResponseData.WriteStruct(applicationAlbumEntry); context.ResponseData.WriteStruct(applicationAlbumEntry);
@ -60,7 +60,7 @@ namespace Ryujinx.HLE.HOS.Services.Caps
byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray(); byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray();
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Application.TitleId, out ApplicationAlbumEntry applicationAlbumEntry); ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
context.ResponseData.WriteStruct(applicationAlbumEntry); context.ResponseData.WriteStruct(applicationAlbumEntry);
@ -88,7 +88,7 @@ namespace Ryujinx.HLE.HOS.Services.Caps
byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray(); byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray();
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Application.TitleId, out ApplicationAlbumEntry applicationAlbumEntry); ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
context.ResponseData.WriteStruct(applicationAlbumEntry); context.ResponseData.WriteStruct(applicationAlbumEntry);

View file

@ -55,7 +55,7 @@ namespace Ryujinx.HLE.HOS.Services.Fatal
errorReport.AppendLine(); errorReport.AppendLine();
errorReport.AppendLine("ErrorReport log:"); errorReport.AppendLine("ErrorReport log:");
errorReport.AppendLine($"\tTitleId: {context.Device.Application.TitleId:x16}"); errorReport.AppendLine($"\tTitleId: {context.Device.Processes.ActiveApplication.ProgramIdText}");
errorReport.AppendLine($"\tPid: {pid}"); errorReport.AppendLine($"\tPid: {pid}");
errorReport.AppendLine($"\tResultCode: {((int)resultCode & 0x1FF) + 2000}-{((int)resultCode >> 9) & 0x3FFF:d4}"); errorReport.AppendLine($"\tResultCode: {((int)resultCode & 0x1FF) + 2000}-{((int)resultCode >> 9) & 0x3FFF:d4}");
errorReport.AppendLine($"\tFatalPolicy: {fatalPolicy}"); errorReport.AppendLine($"\tFatalPolicy: {fatalPolicy}");
@ -64,7 +64,7 @@ namespace Ryujinx.HLE.HOS.Services.Fatal
{ {
errorReport.AppendLine("CPU Context:"); errorReport.AppendLine("CPU Context:");
if (context.Device.Application.TitleIs64Bit) if (context.Device.Processes.ActiveApplication.Is64Bit)
{ {
CpuContext64 cpuContext64 = MemoryMarshal.Cast<byte, CpuContext64>(cpuContext)[0]; CpuContext64 cpuContext64 = MemoryMarshal.Cast<byte, CpuContext64>(cpuContext)[0];

View file

@ -334,7 +334,7 @@ namespace Ryujinx.HLE.HOS.Services.Friend.ServiceCreator
} }
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented. // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Application.ControlData.Value; ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
/* /*

View file

@ -808,7 +808,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs
{ {
byte programIndex = context.RequestData.ReadByte(); byte programIndex = context.RequestData.ReadByte();
if ((context.Device.Application.TitleId & 0xf) != programIndex) if ((context.Device.Processes.ActiveApplication.ProgramId & 0xf) != programIndex)
{ {
throw new NotImplementedException($"Accessing storage from other programs is not supported (program index = {programIndex})."); throw new NotImplementedException($"Accessing storage from other programs is not supported (program index = {programIndex}).");
} }

View file

@ -48,7 +48,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId. // NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
return CountAddOnContentImpl(context, context.Device.Application.TitleId); return CountAddOnContentImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
} }
[CommandHipc(3)] [CommandHipc(3)]
@ -59,7 +59,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId. // NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
return ListAddContentImpl(context, context.Device.Application.TitleId); return ListAddContentImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
} }
[CommandHipc(4)] // 1.0.0-6.2.0 [CommandHipc(4)] // 1.0.0-6.2.0
@ -79,7 +79,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId. // NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
return GetAddOnContentBaseIdImpl(context, context.Device.Application.TitleId); return GetAddOnContentBaseIdImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
} }
[CommandHipc(6)] // 1.0.0-6.2.0 [CommandHipc(6)] // 1.0.0-6.2.0
@ -99,7 +99,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId. // NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
return PrepareAddOnContentImpl(context, context.Device.Application.TitleId); return PrepareAddOnContentImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
} }
[CommandHipc(8)] // 4.0.0+ [CommandHipc(8)] // 4.0.0+
@ -128,7 +128,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId. // NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
// TODO: Found where stored value is used. // TODO: Found where stored value is used.
ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, context.Device.Application.TitleId); ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, context.Device.Processes.ActiveApplication.ProgramId);
if (resultCode != ResultCode.Success) if (resultCode != ResultCode.Success)
{ {
@ -294,7 +294,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service calls arp:r GetApplicationControlProperty to get AddOnContentBaseId using TitleId, // NOTE: Service calls arp:r GetApplicationControlProperty to get AddOnContentBaseId using TitleId,
// If the call fails, it returns ResultCode.InvalidPid. // If the call fails, it returns ResultCode.InvalidPid.
_addOnContentBaseId = context.Device.Application.ControlData.Value.AddOnContentBaseId; _addOnContentBaseId = context.Device.Processes.ActiveApplication.ApplicationControlProperties.AddOnContentBaseId;
if (_addOnContentBaseId == 0) if (_addOnContentBaseId == 0)
{ {
@ -308,7 +308,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
{ {
uint index = context.RequestData.ReadUInt32(); uint index = context.RequestData.ReadUInt32();
ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, context.Device.Application.TitleId); ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, context.Device.Processes.ActiveApplication.ProgramId);
if (resultCode != ResultCode.Success) if (resultCode != ResultCode.Success)
{ {

View file

@ -1,4 +1,8 @@
namespace Ryujinx.HLE.HOS.Services.Ns using LibHac.Ns;
using Ryujinx.Common.Utilities;
using System;
namespace Ryujinx.HLE.HOS.Services.Ns
{ {
[Service("ns:am")] [Service("ns:am")]
class IApplicationManagerInterface : IpcService class IApplicationManagerInterface : IpcService
@ -14,9 +18,9 @@
ulong position = context.Request.ReceiveBuff[0].Position; ulong position = context.Request.ReceiveBuff[0].Position;
byte[] nacpData = context.Device.Application.ControlData.ByteSpan.ToArray(); ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
context.Memory.Write(position, nacpData); context.Memory.Write(position, SpanHelpers.AsByteSpan(ref nacp).ToArray());
return ResultCode.Success; return ResultCode.Success;
} }

View file

@ -1,4 +1,7 @@
namespace Ryujinx.HLE.HOS.Services.Ns using LibHac.Common;
using LibHac.Ns;
namespace Ryujinx.HLE.HOS.Services.Ns
{ {
class IReadOnlyApplicationControlDataInterface : IpcService class IReadOnlyApplicationControlDataInterface : IpcService
{ {
@ -13,9 +16,9 @@
ulong position = context.Request.ReceiveBuff[0].Position; ulong position = context.Request.ReceiveBuff[0].Position;
byte[] nacpData = context.Device.Application.ControlData.ByteSpan.ToArray(); ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
context.Memory.Write(position, nacpData); context.Memory.Write(position, SpanHelpers.AsByteSpan(ref nacp).ToArray());
return ResultCode.Success; return ResultCode.Success;
} }

View file

@ -56,8 +56,8 @@ namespace Ryujinx.HLE.HOS.Services.Pctl.ParentalControlServiceFactory
_titleId = titleId; _titleId = titleId;
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented, if it return ResultCode.Success we assign fields. // TODO: Call nn::arp::GetApplicationControlProperty here when implemented, if it return ResultCode.Success we assign fields.
_ratingAge = Array.ConvertAll(context.Device.Application.ControlData.Value.RatingAge.ItemsRo.ToArray(), Convert.ToInt32); _ratingAge = Array.ConvertAll(context.Device.Processes.ActiveApplication.ApplicationControlProperties.RatingAge.ItemsRo.ToArray(), Convert.ToInt32);
_parentalControlFlag = context.Device.Application.ControlData.Value.ParentalControlFlag; _parentalControlFlag = context.Device.Processes.ActiveApplication.ApplicationControlProperties.ParentalControlFlag;
} }
} }

View file

@ -6,7 +6,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
{ {
@ -16,8 +15,6 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
internal static ResultCode GetPlayStatistics(ServiceCtx context, bool byUserId = false) internal static ResultCode GetPlayStatistics(ServiceCtx context, bool byUserId = false)
{ {
ref readonly var controlProperty = ref context.Device.Application.ControlData.Value;
ulong inputPosition = context.Request.SendBuff[0].Position; ulong inputPosition = context.Request.SendBuff[0].Position;
ulong inputSize = context.Request.SendBuff[0].Size; ulong inputSize = context.Request.SendBuff[0].Size;
@ -34,7 +31,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
} }
} }
PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)controlProperty.PlayLogQueryCapability; PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.Processes.ActiveApplication.ApplicationControlProperties.PlayLogQueryCapability;
List<ulong> titleIds = new List<ulong>(); List<ulong> titleIds = new List<ulong>();
@ -48,7 +45,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
// Check if input title ids are in the whitelist. // Check if input title ids are in the whitelist.
foreach (ulong titleId in titleIds) foreach (ulong titleId in titleIds)
{ {
if (!controlProperty.PlayLogQueryableApplicationId.ItemsRo.Contains(titleId)) if (!context.Device.Processes.ActiveApplication.ApplicationControlProperties.PlayLogQueryableApplicationId.ItemsRo.Contains(titleId))
{ {
return (ResultCode)Am.ResultCode.ObjectInvalid; return (ResultCode)Am.ResultCode.ObjectInvalid;
} }

View file

@ -0,0 +1,133 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Loader;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.Memory;
using System.Linq;
using static Ryujinx.HLE.HOS.ModLoader;
namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
static class FileSystemExtensions
{
public static MetaLoader GetNpdm(this IFileSystem fileSystem)
{
MetaLoader metaLoader = new();
if (fileSystem == null || !fileSystem.FileExists(ProcessConst.MainNpdmPath))
{
Logger.Warning?.Print(LogClass.Loader, "NPDM file not found, using default values!");
metaLoader.LoadDefault();
}
else
{
metaLoader.LoadFromFile(fileSystem);
}
return metaLoader;
}
public static ProcessResult Load(this IFileSystem exeFs, Switch device, BlitStruct<ApplicationControlProperty> nacpData, MetaLoader metaLoader, bool isHomebrew = false)
{
ulong programId = metaLoader.GetProgramId();
// Replace the whole ExeFs partition by the modded one.
if (device.Configuration.VirtualFileSystem.ModLoader.ReplaceExefsPartition(programId, ref exeFs))
{
metaLoader = null;
}
// Reload the MetaLoader in case of ExeFs partition replacement.
metaLoader ??= exeFs.GetNpdm();
NsoExecutable[] nsoExecutables = new NsoExecutable[ProcessConst.ExeFsPrefixes.Length];
for (int i = 0; i < nsoExecutables.Length; i++)
{
string name = ProcessConst.ExeFsPrefixes[i];
if (!exeFs.FileExists($"/{name}"))
{
continue; // File doesn't exist, skip.
}
Logger.Info?.Print(LogClass.Loader, $"Loading {name}...");
using var nsoFile = new UniqueRef<IFile>();
exeFs.OpenFile(ref nsoFile.Ref, $"/{name}".ToU8Span(), OpenMode.Read).ThrowIfFailure();
nsoExecutables[i] = new NsoExecutable(nsoFile.Release().AsStorage(), name);
}
// ExeFs file replacements.
ModLoadResult modLoadResult = device.Configuration.VirtualFileSystem.ModLoader.ApplyExefsMods(programId, nsoExecutables);
// Take the Npdm from mods if present.
if (modLoadResult.Npdm != null)
{
metaLoader = modLoadResult.Npdm;
}
// Collect the Nsos, ignoring ones that aren't used.
nsoExecutables = nsoExecutables.Where(x => x != null).ToArray();
// Apply Nsos patches.
device.Configuration.VirtualFileSystem.ModLoader.ApplyNsoPatches(programId, nsoExecutables);
// Don't use PTC if ExeFS files have been replaced.
bool enablePtc = device.System.EnablePtc && !modLoadResult.Modified;
if (!enablePtc)
{
Logger.Warning?.Print(LogClass.Ptc, $"Detected unsupported ExeFs modifications. PTC disabled.");
}
// We allow it for nx-hbloader because it can be used to launch homebrew.
bool allowCodeMemoryForJit = programId == 0x010000000000100DUL || isHomebrew;
string programName = "";
if (!isHomebrew && programId > 0x010000000000FFFF)
{
programName = nacpData.Value.Title[(int)device.System.State.DesiredTitleLanguage].NameString.ToString();
if (string.IsNullOrWhiteSpace(programName))
{
programName = nacpData.Value.Title.ItemsRo.ToArray().FirstOrDefault(x => x.Name[0] != 0).NameString.ToString();
}
}
// Initialize GPU.
Graphics.Gpu.GraphicsConfig.TitleId = $"{programId:x16}";
device.Gpu.HostInitalized.Set();
if (!MemoryBlock.SupportsFlags(MemoryAllocationFlags.ViewCompatible))
{
device.Configuration.MemoryManagerMode = MemoryManagerMode.SoftwarePageTable;
}
ProcessResult processResult = ProcessLoaderHelper.LoadNsos(
device,
device.System.KernelContext,
metaLoader,
nacpData.Value,
enablePtc,
allowCodeMemoryForJit,
programName,
metaLoader.GetProgramId(),
null,
nsoExecutables);
// TODO: This should be stored using ProcessId instead.
device.System.LibHacHorizonManager.ArpIReader.ApplicationId = new LibHac.ApplicationId(metaLoader.GetProgramId());
return processResult;
}
}
}

View file

@ -0,0 +1,39 @@
using LibHac.Common;
using LibHac.FsSystem;
using LibHac.Loader;
using LibHac.Ns;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using ApplicationId = LibHac.Ncm.ApplicationId;
namespace Ryujinx.HLE.Loaders.Processes
{
static class LocalFileSystemExtensions
{
public static ProcessResult Load(this LocalFileSystem exeFs, Switch device, string romFsPath = "")
{
MetaLoader metaLoader = exeFs.GetNpdm();
var nacpData = new BlitStruct<ApplicationControlProperty>(1);
ulong programId = metaLoader.GetProgramId();
device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
new[] { programId },
device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(),
device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath());
if (programId != 0)
{
ProcessLoaderHelper.EnsureSaveData(device, new ApplicationId(programId), nacpData);
}
ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader);
// Load RomFS.
if (!string.IsNullOrEmpty(romFsPath))
{
device.Configuration.VirtualFileSystem.LoadRomFs(processResult.ProcessId, romFsPath);
}
return processResult;
}
}
}

View file

@ -0,0 +1,61 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Loader;
using LibHac.Util;
using Ryujinx.Common;
using System;
namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
public static class MetaLoaderExtensions
{
public static ulong GetProgramId(this MetaLoader metaLoader)
{
metaLoader.GetNpdm(out var npdm).ThrowIfFailure();
return npdm.Aci.ProgramId.Value;
}
public static string GetProgramName(this MetaLoader metaLoader)
{
metaLoader.GetNpdm(out var npdm).ThrowIfFailure();
return StringUtils.Utf8ZToString(npdm.Meta.ProgramName);
}
public static bool IsProgram64Bit(this MetaLoader metaLoader)
{
metaLoader.GetNpdm(out var npdm).ThrowIfFailure();
return (npdm.Meta.Flags & 1) != 0;
}
public static void LoadDefault(this MetaLoader metaLoader)
{
byte[] npdmBuffer = EmbeddedResources.Read("Ryujinx.HLE/Homebrew.npdm");
metaLoader.Load(npdmBuffer).ThrowIfFailure();
}
public static void LoadFromFile(this MetaLoader metaLoader, IFileSystem fileSystem, string path = "")
{
if (string.IsNullOrEmpty(path))
{
path = ProcessConst.MainNpdmPath;
}
using var npdmFile = new UniqueRef<IFile>();
fileSystem.OpenFile(ref npdmFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
npdmFile.Get.GetSize(out long fileSize).ThrowIfFailure();
Span<byte> npdmBuffer = new byte[fileSize];
npdmFile.Get.Read(out _, 0, npdmBuffer).ThrowIfFailure();
metaLoader.Load(npdmBuffer).ThrowIfFailure();
}
}
}

View file

@ -0,0 +1,175 @@
using LibHac;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Loader;
using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using System.IO;
using System.Linq;
using ApplicationId = LibHac.Ncm.ApplicationId;
namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
static class NcaExtensions
{
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
{
// Extract RomFs and ExeFs from NCA.
IStorage romFs = nca.GetRomFs(device, patchNca);
IFileSystem exeFs = nca.GetExeFs(device, patchNca);
if (exeFs == null)
{
Logger.Error?.Print(LogClass.Loader, "No ExeFS found in NCA");
return ProcessResult.Failed;
}
// Load Npdm file.
MetaLoader metaLoader = exeFs.GetNpdm();
// Collecting mods related to AocTitleIds and ProgramId.
device.Configuration.VirtualFileSystem.ModLoader.CollectMods(
device.Configuration.ContentManager.GetAocTitleIds().Prepend(metaLoader.GetProgramId()),
device.Configuration.VirtualFileSystem.ModLoader.GetModsBasePath(),
device.Configuration.VirtualFileSystem.ModLoader.GetSdModsBasePath());
// Load Nacp file.
var nacpData = new BlitStruct<ApplicationControlProperty>(1);
if (controlNca != null)
{
nacpData = controlNca.GetNacp(device);
}
/* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update.
// Load program 0 control NCA as we are going to need it for display version.
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
// NOTE: Nintendo doesn't guarantee that the display version will be updated on sub programs when updating a multi program application.
// As such, to avoid PTC cache confusion, we only trust the program 0 display version when launching a sub program.
if (updateProgram0ControlNca != null && _device.Configuration.UserChannelPersistence.Index != 0)
{
nacpData.Value.DisplayVersion = updateProgram0ControlNca.GetNacp(_device).Value.DisplayVersion;
}
*/
ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader);
// Load RomFS.
if (romFs == null)
{
Logger.Warning?.Print(LogClass.Loader, "No RomFS found in NCA");
}
else
{
romFs = device.Configuration.VirtualFileSystem.ModLoader.ApplyRomFsMods(processResult.ProgramId, romFs);
device.Configuration.VirtualFileSystem.SetRomFs(processResult.ProcessId, romFs.AsStream(FileAccess.Read));
}
// Don't create save data for system programs.
if (processResult.ProgramId != 0 && (processResult.ProgramId < SystemProgramId.Start.Value || processResult.ProgramId > SystemAppletId.End.Value))
{
// Multi-program applications can technically use any program ID for the main program, but in practice they always use 0 in the low nibble.
// We'll know if this changes in the future because applications will get errors when trying to mount the correct save.
ProcessLoaderHelper.EnsureSaveData(device, new ApplicationId(processResult.ProgramId & ~0xFul), nacpData);
}
return processResult;
}
public static int GetProgramIndex(this Nca nca)
{
return (int)(nca.Header.TitleId & 0xF);
}
public static bool IsProgram(this Nca nca)
{
return nca.Header.ContentType == NcaContentType.Program;
}
public static bool IsPatch(this Nca nca)
{
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
return nca.IsProgram() && nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection();
}
public static bool IsControl(this Nca nca)
{
return nca.Header.ContentType == NcaContentType.Control;
}
public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
{
IFileSystem exeFs = null;
if (patchNca == null)
{
if (nca.CanOpenSection(NcaSectionType.Code))
{
exeFs = nca.OpenFileSystem(NcaSectionType.Code, device.System.FsIntegrityCheckLevel);
}
}
else
{
if (patchNca.CanOpenSection(NcaSectionType.Code))
{
exeFs = nca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, device.System.FsIntegrityCheckLevel);
}
}
return exeFs;
}
public static IStorage GetRomFs(this Nca nca, Switch device, Nca patchNca = null)
{
IStorage romFs = null;
if (patchNca == null)
{
if (nca.CanOpenSection(NcaSectionType.Data))
{
romFs = nca.OpenStorage(NcaSectionType.Data, device.System.FsIntegrityCheckLevel);
}
}
else
{
if (patchNca.CanOpenSection(NcaSectionType.Data))
{
romFs = nca.OpenStorageWithPatch(patchNca, NcaSectionType.Data, device.System.FsIntegrityCheckLevel);
}
}
return romFs;
}
public static BlitStruct<ApplicationControlProperty> GetNacp(this Nca controlNca, Switch device)
{
var nacpData = new BlitStruct<ApplicationControlProperty>(1);
using var controlFile = new UniqueRef<IFile>();
Result result = controlNca.OpenFileSystem(NcaSectionType.Data, device.System.FsIntegrityCheckLevel)
.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
result = controlFile.Get.Read(out long bytesRead, 0, nacpData.ByteSpan, ReadOption.None);
}
else
{
nacpData.ByteSpan.Clear();
}
return nacpData;
}
}
}

View file

@ -0,0 +1,177 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
public static class PartitionFileSystemExtensions
{
internal static (bool, ProcessResult) TryLoad(this PartitionFileSystem partitionFileSystem, Switch device, string path, out string errorMessage)
{
errorMessage = null;
// Load required NCAs.
Nca mainNca = null;
Nca patchNca = null;
Nca controlNca = null;
try
{
device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem);
// TODO: To support multi-games container, this should use CNMT NCA instead.
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
{
Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
{
continue;
}
if (nca.IsPatch())
{
patchNca = nca;
}
else if (nca.IsProgram())
{
mainNca = nca;
}
else if (nca.IsControl())
{
controlNca = nca;
}
}
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
}
catch (Exception ex)
{
errorMessage = $"Unable to load: {ex.Message}";
return (false, ProcessResult.Failed);
}
if (mainNca != null)
{
if (mainNca.Header.ContentType != NcaContentType.Program)
{
errorMessage = "Selected NCA file is not a \"Program\" NCA";
return (false, ProcessResult.Failed);
}
// Load Update NCAs.
Nca updatePatchNca = null;
Nca updateControlNca = null;
if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
{
// Clear the program index part.
titleIdBase &= ~0xFUL;
// Load update information if exists.
string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
if (File.Exists(titleUpdateMetadataPath))
{
string updatePath = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(titleUpdateMetadataPath).Selected;
if (File.Exists(updatePath))
{
PartitionFileSystem updatePartitionFileSystem = new(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage());
device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
// TODO: This should use CNMT NCA instead.
foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca"))
{
Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath);
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
{
continue;
}
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16"))
{
break;
}
if (nca.IsProgram())
{
updatePatchNca = nca;
}
else if (nca.IsControl())
{
updateControlNca = nca;
}
}
}
}
}
if (updatePatchNca != null)
{
patchNca = updatePatchNca;
}
if (updateControlNca != null)
{
controlNca = updateControlNca;
}
// Load contained DownloadableContents.
// TODO: If we want to support multi-processes in future, we shouldn't clear AddOnContent data here.
device.Configuration.ContentManager.ClearAocData();
device.Configuration.ContentManager.AddAocData(partitionFileSystem, path, mainNca.Header.TitleId, device.Configuration.FsIntegrityCheckLevel);
// Load DownloadableContents.
string addOnContentMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json");
if (File.Exists(addOnContentMetadataPath))
{
List<DownloadableContentContainer> dlcContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(addOnContentMetadataPath);
foreach (DownloadableContentContainer downloadableContentContainer in dlcContainerList)
{
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
{
if (File.Exists(downloadableContentContainer.ContainerPath) && downloadableContentNca.Enabled)
{
device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath);
}
else
{
Logger.Warning?.Print(LogClass.Application, $"Cannot find AddOnContent file {downloadableContentContainer.ContainerPath}. It may have been moved or renamed.");
}
}
}
}
return (true, mainNca.Load(device, patchNca, controlNca));
}
errorMessage = "Unable to load: Could not find Main NCA";
return (false, ProcessResult.Failed);
}
public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path)
{
using var ncaFile = new UniqueRef<IFile>();
fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage());
}
}
}

View file

@ -0,0 +1,33 @@
namespace Ryujinx.HLE.Loaders.Processes
{
static class ProcessConst
{
// Binaries from exefs are loaded into mem in this order. Do not change.
public static readonly string[] ExeFsPrefixes =
{
"rtld",
"main",
"subsdk0",
"subsdk1",
"subsdk2",
"subsdk3",
"subsdk4",
"subsdk5",
"subsdk6",
"subsdk7",
"subsdk8",
"subsdk9",
"sdk"
};
public static readonly string MainNpdmPath = "/main.npdm";
public const int NroAsetMagic = ('A' << 0) | ('S' << 8) | ('E' << 16) | ('T' << 24);
public const bool AslrEnabled = true;
public const int NsoArgsHeaderSize = 8;
public const int NsoArgsDataSize = 0x9000;
public const int NsoArgsTotalSize = NsoArgsHeaderSize + NsoArgsDataSize;
}
}

View file

@ -0,0 +1,244 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using Path = System.IO.Path;
namespace Ryujinx.HLE.Loaders.Processes
{
public class ProcessLoader
{
private readonly Switch _device;
private readonly ConcurrentDictionary<ulong, ProcessResult> _processesByPid;
private ulong _latestPid;
public ProcessResult ActiveApplication => _processesByPid[_latestPid];
public ProcessLoader(Switch device)
{
_device = device;
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>();
}
public bool LoadXci(string path)
{
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
if (!xci.HasPartition(XciPartitionType.Secure))
{
Logger.Error?.Print(LogClass.Loader, "Unable to load XCI: Could not find XCI Secure partition");
return false;
}
(bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage);
if (!success)
{
Logger.Error?.Print(LogClass.Loader, errorMessage, nameof(PartitionFileSystemExtensions.TryLoad));
return false;
}
if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult))
{
if (processResult.Start(_device))
{
_latestPid = processResult.ProcessId;
return true;
}
}
return false;
}
public bool LoadNsp(string path)
{
FileStream file = new(path, FileMode.Open, FileAccess.Read);
PartitionFileSystem partitionFileSystem = new(file.AsStorage());
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage);
if (processResult.ProcessId == 0)
{
// This is not a normal NSP, it's actually a ExeFS as a NSP
processResult = partitionFileSystem.Load(_device, new BlitStruct<ApplicationControlProperty>(1), partitionFileSystem.GetNpdm(), true);
}
if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult))
{
if (processResult.Start(_device))
{
_latestPid = processResult.ProcessId;
return true;
}
}
if (!success)
{
Logger.Error?.Print(LogClass.Loader, errorMessage, nameof(PartitionFileSystemExtensions.TryLoad));
}
return false;
}
public bool LoadNca(string path)
{
FileStream file = new(path, FileMode.Open, FileAccess.Read);
Nca nca = new(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage(false));
ProcessResult processResult = nca.Load(_device, null, null);
if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult))
{
if (processResult.Start(_device))
{
// NOTE: Check if process is SystemApplicationId or ApplicationId
if (processResult.ProgramId > 0x01000000000007FF)
{
_latestPid = processResult.ProcessId;
}
return true;
}
}
return false;
}
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))
{
_latestPid = processResult.ProcessId;
return true;
}
}
return false;
}
public bool LoadNxo(string path)
{
var nacpData = new BlitStruct<ApplicationControlProperty>(1);
IFileSystem dummyExeFs = null;
Stream romfsStream = null;
string programName = "";
ulong programId = 0000000000000000;
// Load executable.
IExecutable executable;
if (Path.GetExtension(path).ToLower() == ".nro")
{
FileStream input = new(path, FileMode.Open);
NroExecutable nro = new(input.AsStorage());
executable = nro;
// Open RomFS if exists.
IStorage romFsStorage = nro.OpenNroAssetSection(LibHac.Tools.Ro.NroAssetType.RomFs, false);
romFsStorage.GetSize(out long romFsSize).ThrowIfFailure();
if (romFsSize != 0)
{
romfsStream = romFsStorage.AsStream();
}
// Load Nacp if exists.
IStorage nacpStorage = nro.OpenNroAssetSection(LibHac.Tools.Ro.NroAssetType.Nacp, false);
nacpStorage.GetSize(out long nacpSize).ThrowIfFailure();
if (nacpSize != 0)
{
nacpStorage.Read(0, nacpData.ByteSpan);
programName = nacpData.Value.Title[(int)_device.System.State.DesiredTitleLanguage].NameString.ToString();
if (string.IsNullOrWhiteSpace(programName))
{
programName = nacpData.Value.Title.ItemsRo.ToArray().FirstOrDefault(x => x.Name[0] != 0).NameString.ToString();
}
if (nacpData.Value.PresenceGroupId != 0)
{
programId = nacpData.Value.PresenceGroupId;
}
else if (nacpData.Value.SaveDataOwnerId != 0)
{
programId = nacpData.Value.SaveDataOwnerId;
}
else if (nacpData.Value.AddOnContentBaseId != 0)
{
programId = nacpData.Value.AddOnContentBaseId - 0x1000;
}
}
// TODO: Add icon maybe ?
}
else
{
programName = System.IO.Path.GetFileNameWithoutExtension(path);
executable = new NsoExecutable(new LocalStorage(path, FileAccess.Read), programName);
}
// Explicitly null TitleId to disable the shader cache.
Graphics.Gpu.GraphicsConfig.TitleId = null;
_device.Gpu.HostInitalized.Set();
ProcessResult processResult = ProcessLoaderHelper.LoadNsos(_device,
_device.System.KernelContext,
dummyExeFs.GetNpdm(),
nacpData.Value,
diskCacheEnabled: false,
allowCodeMemoryForJit: true,
programName,
programId,
null,
executable);
// Make sure the process id is valid.
if (processResult.ProcessId != 0)
{
// Load RomFS.
if (romfsStream != null)
{
_device.Configuration.VirtualFileSystem.SetRomFs(processResult.ProcessId, romfsStream);
}
// Start process.
if (_processesByPid.TryAdd(processResult.ProcessId, processResult))
{
if (processResult.Start(_device))
{
_latestPid = processResult.ProcessId;
return true;
}
}
}
return false;
}
}
}

View file

@ -1,69 +1,132 @@
using LibHac.Account;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Shim;
using LibHac.FsSystem;
using LibHac.Loader; using LibHac.Loader;
using LibHac.Ncm; using LibHac.Ncm;
using LibHac.Util; using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Cpu; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Kernel; using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Memory; using Ryujinx.HLE.HOS.Kernel.Memory;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
using System; using System;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Npdm = LibHac.Loader.Npdm; using ApplicationId = LibHac.Ncm.ApplicationId;
namespace Ryujinx.HLE.HOS namespace Ryujinx.HLE.Loaders.Processes
{ {
struct ProgramInfo static class ProcessLoaderHelper
{ {
public string Name; public static LibHac.Result RegisterProgramMapInfo(Switch device, PartitionFileSystem partitionFileSystem)
public ulong ProgramId;
public readonly string TitleIdText;
public readonly string DisplayVersion;
public readonly bool DiskCacheEnabled;
public readonly bool AllowCodeMemoryForJit;
public ProgramInfo(in Npdm npdm, string displayVersion, bool diskCacheEnabled, bool allowCodeMemoryForJit)
{ {
ulong programId = npdm.Aci.ProgramId.Value; ulong applicationId = 0;
int programCount = 0;
Name = StringUtils.Utf8ZToString(npdm.Meta.ProgramName); Span<bool> hasIndex = stackalloc bool[0x10];
ProgramId = programId;
TitleIdText = programId.ToString("x16"); foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
DisplayVersion = displayVersion; {
DiskCacheEnabled = diskCacheEnabled; Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
AllowCodeMemoryForJit = allowCodeMemoryForJit;
if (!nca.IsProgram() && nca.IsPatch())
{
continue;
}
ulong currentProgramId = nca.Header.TitleId;
ulong currentMainProgramId = currentProgramId & ~0xFFFul;
if (applicationId == 0 && currentMainProgramId != 0)
{
applicationId = currentMainProgramId;
}
if (applicationId != currentMainProgramId)
{
// Currently there aren't any known multi-application game cards containing multi-program applications,
// so because multi-application game cards are the only way we could run into multiple applications
// we'll just return that there's a single program.
programCount = 1;
break;
}
hasIndex[(int)(currentProgramId & 0xF)] = true;
}
if (programCount == 0)
{
for (int i = 0; i < hasIndex.Length && hasIndex[i]; i++)
{
programCount++;
}
}
if (programCount <= 0)
{
return LibHac.Result.Success;
}
Span<ProgramIndexMapInfo> mapInfo = stackalloc ProgramIndexMapInfo[0x10];
for (int i = 0; i < programCount; i++)
{
mapInfo[i].ProgramId = new ProgramId(applicationId + (uint)i);
mapInfo[i].MainProgramId = new ApplicationId(applicationId);
mapInfo[i].ProgramIndex = (byte)i;
}
return device.System.LibHacHorizonManager.NsClient.Fs.RegisterProgramIndexMapInfo(mapInfo[..programCount]);
} }
}
struct ProgramLoadResult public static LibHac.Result EnsureSaveData(Switch device, ApplicationId applicationId, BlitStruct<ApplicationControlProperty> applicationControlProperty)
{
public static ProgramLoadResult Failed => new ProgramLoadResult(false, null, null, 0);
public readonly bool Success;
public readonly ProcessTamperInfo TamperInfo;
public readonly IDiskCacheLoadState DiskCacheLoadState;
public readonly ulong ProcessId;
public ProgramLoadResult(bool success, ProcessTamperInfo tamperInfo, IDiskCacheLoadState diskCacheLoadState, ulong pid)
{ {
Success = success; Logger.Info?.Print(LogClass.Application, "Ensuring required savedata exists.");
TamperInfo = tamperInfo;
DiskCacheLoadState = diskCacheLoadState; ref ApplicationControlProperty control = ref applicationControlProperty.Value;
ProcessId = pid;
if (LibHac.Common.Utilities.IsZeros(applicationControlProperty.ByteSpan))
{
// If the current application doesn't have a loaded control property, create a dummy one and set the savedata sizes so a user savedata will be created.
control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
// The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
control.UserAccountSaveDataSize = 0x4000;
control.UserAccountSaveDataJournalSize = 0x4000;
control.SaveDataOwnerId = applicationId.Value;
Logger.Warning?.Print(LogClass.Application, "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
}
LibHac.Result resultCode = device.System.LibHacHorizonManager.RyujinxClient.Fs.EnsureApplicationCacheStorage(out _, out _, applicationId, in control);
if (resultCode.IsFailure())
{
Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationCacheStorage. Result code {resultCode.ToStringWithName()}");
return resultCode;
}
Uid userId = device.System.AccountManager.LastOpenedUser.UserId.ToLibHacUid();
resultCode = device.System.LibHacHorizonManager.RyujinxClient.Fs.EnsureApplicationSaveData(out _, applicationId, in control, in userId);
if (resultCode.IsFailure())
{
Logger.Error?.Print(LogClass.Application, $"Error calling EnsureApplicationSaveData. Result code {resultCode.ToStringWithName()}");
}
return resultCode;
} }
}
static class ProgramLoader
{
private const bool AslrEnabled = true;
private const int ArgsHeaderSize = 8;
private const int ArgsDataSize = 0x9000;
private const int ArgsTotalSize = ArgsHeaderSize + ArgsDataSize;
public static bool LoadKip(KernelContext context, KipExecutable kip) public static bool LoadKip(KernelContext context, KipExecutable kip)
{ {
@ -74,17 +137,14 @@ namespace Ryujinx.HLE.HOS
endOffset = kip.BssOffset + kip.BssSize; endOffset = kip.BssOffset + kip.BssSize;
} }
uint codeSize = BitUtils.AlignUp<uint>(kip.TextOffset + endOffset, KPageTableBase.PageSize); uint codeSize = BitUtils.AlignUp<uint>(kip.TextOffset + endOffset, KPageTableBase.PageSize);
int codePagesCount = (int)(codeSize / KPageTableBase.PageSize);
int codePagesCount = (int)(codeSize / KPageTableBase.PageSize);
ulong codeBaseAddress = kip.Is64BitAddressSpace ? 0x8000000UL : 0x200000UL; ulong codeBaseAddress = kip.Is64BitAddressSpace ? 0x8000000UL : 0x200000UL;
ulong codeAddress = codeBaseAddress + kip.TextOffset;
ulong codeAddress = codeBaseAddress + kip.TextOffset;
ProcessCreationFlags flags = 0; ProcessCreationFlags flags = 0;
if (AslrEnabled) if (ProcessConst.AslrEnabled)
{ {
// TODO: Randomization. // TODO: Randomization.
@ -101,24 +161,11 @@ namespace Ryujinx.HLE.HOS
flags |= ProcessCreationFlags.Is64Bit; flags |= ProcessCreationFlags.Is64Bit;
} }
ProcessCreationInfo creationInfo = new ProcessCreationInfo( ProcessCreationInfo creationInfo = new(kip.Name, kip.Version, kip.ProgramId, codeAddress, codePagesCount, flags, 0, 0);
kip.Name, MemoryRegion memoryRegion = kip.UsesSecureMemory ? MemoryRegion.Service : MemoryRegion.Application;
kip.Version, KMemoryRegionManager region = context.MemoryManager.MemoryRegions[(int)memoryRegion];
kip.ProgramId,
codeAddress,
codePagesCount,
flags,
0,
0);
MemoryRegion memoryRegion = kip.UsesSecureMemory
? MemoryRegion.Service
: MemoryRegion.Application;
KMemoryRegionManager region = context.MemoryManager.MemoryRegions[(int)memoryRegion];
Result result = region.AllocatePages(out KPageList pageList, (ulong)codePagesCount); Result result = region.AllocatePages(out KPageList pageList, (ulong)codePagesCount);
if (result != Result.Success) if (result != Result.Success)
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\"."); Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\".");
@ -126,7 +173,7 @@ namespace Ryujinx.HLE.HOS
return false; return false;
} }
KProcess process = new KProcess(context); KProcess process = new(context);
var processContextFactory = new ArmProcessContextFactory( var processContextFactory = new ArmProcessContextFactory(
context.Device.System.TickSource, context.Device.System.TickSource,
@ -137,14 +184,7 @@ namespace Ryujinx.HLE.HOS
codeAddress, codeAddress,
codeSize); codeSize);
result = process.InitializeKip( result = process.InitializeKip(creationInfo, kip.Capabilities, pageList, context.ResourceLimit, memoryRegion, processContextFactory);
creationInfo,
kip.Capabilities,
pageList,
context.ResourceLimit,
memoryRegion,
processContextFactory);
if (result != Result.Success) if (result != Result.Success)
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\"."); Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\".");
@ -153,7 +193,6 @@ namespace Ryujinx.HLE.HOS
} }
result = LoadIntoMemory(process, kip, codeBaseAddress); result = LoadIntoMemory(process, kip, codeBaseAddress);
if (result != Result.Success) if (result != Result.Success)
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\"."); Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\".");
@ -164,7 +203,6 @@ namespace Ryujinx.HLE.HOS
process.DefaultCpuCore = kip.IdealCoreId; process.DefaultCpuCore = kip.IdealCoreId;
result = process.Start(kip.Priority, (ulong)kip.StackSize); result = process.Start(kip.Priority, (ulong)kip.StackSize);
if (result != Result.Success) if (result != Result.Success)
{ {
Logger.Error?.Print(LogClass.Loader, $"Process start returned error \"{result}\"."); Logger.Error?.Print(LogClass.Loader, $"Process start returned error \"{result}\".");
@ -177,20 +215,27 @@ namespace Ryujinx.HLE.HOS
return true; return true;
} }
public static ProgramLoadResult LoadNsos( public static ProcessResult LoadNsos(
Switch device,
KernelContext context, KernelContext context,
MetaLoader metaData, MetaLoader metaLoader,
ProgramInfo programInfo, ApplicationControlProperty applicationControlProperties,
bool diskCacheEnabled,
bool allowCodeMemoryForJit,
string name,
ulong programId,
byte[] arguments = null, byte[] arguments = null,
params IExecutable[] executables) params IExecutable[] executables)
{ {
context.Device.System.ServiceTable.WaitServicesReady(); context.Device.System.ServiceTable.WaitServicesReady();
LibHac.Result rc = metaData.GetNpdm(out var npdm); LibHac.Result resultCode = metaLoader.GetNpdm(out var npdm);
if (rc.IsFailure()) if (resultCode.IsFailure())
{ {
return ProgramLoadResult.Failed; Logger.Error?.Print(LogClass.Loader, $"Process initialization failed getting npdm. Result Code {resultCode.ToStringWithName()}");
return ProcessResult.Failed;
} }
ref readonly var meta = ref npdm.Meta; ref readonly var meta = ref npdm.Meta;
@ -202,10 +247,10 @@ namespace Ryujinx.HLE.HOS
var buildIds = executables.Select(e => (e switch var buildIds = executables.Select(e => (e switch
{ {
NsoExecutable nso => BitConverter.ToString(nso.BuildId.ItemsRo.ToArray()), NsoExecutable nso => Convert.ToHexString(nso.BuildId.ItemsRo.ToArray()),
NroExecutable nro => BitConverter.ToString(nro.Header.BuildId), NroExecutable nro => Convert.ToHexString(nro.Header.BuildId),
_ => "" _ => ""
}).Replace("-", "").ToUpper()); }).ToUpper());
ulong[] nsoBase = new ulong[executables.Length]; ulong[] nsoBase = new ulong[executables.Length];
@ -214,7 +259,7 @@ namespace Ryujinx.HLE.HOS
IExecutable nso = executables[index]; IExecutable nso = executables[index];
uint textEnd = nso.TextOffset + (uint)nso.Text.Length; uint textEnd = nso.TextOffset + (uint)nso.Text.Length;
uint roEnd = nso.RoOffset + (uint)nso.Ro.Length; uint roEnd = nso.RoOffset + (uint)nso.Ro.Length;
uint dataEnd = nso.DataOffset + (uint)nso.Data.Length + nso.BssSize; uint dataEnd = nso.DataOffset + (uint)nso.Data.Length + nso.BssSize;
uint nsoSize = textEnd; uint nsoSize = textEnd;
@ -239,31 +284,30 @@ namespace Ryujinx.HLE.HOS
{ {
argsStart = codeSize; argsStart = codeSize;
argsSize = (uint)BitUtils.AlignDown(arguments.Length * 2 + ArgsTotalSize - 1, KPageTableBase.PageSize); argsSize = (uint)BitUtils.AlignDown(arguments.Length * 2 + ProcessConst.NsoArgsTotalSize - 1, KPageTableBase.PageSize);
codeSize += argsSize; codeSize += argsSize;
} }
} }
int codePagesCount = (int)(codeSize / KPageTableBase.PageSize); int codePagesCount = (int)(codeSize / KPageTableBase.PageSize);
int personalMmHeapPagesCount = (int)(meta.SystemResourceSize / KPageTableBase.PageSize); int personalMmHeapPagesCount = (int)(meta.SystemResourceSize / KPageTableBase.PageSize);
ProcessCreationInfo creationInfo = new ProcessCreationInfo( ProcessCreationInfo creationInfo = new(
programInfo.Name, name,
(int)meta.Version, (int)meta.Version,
programInfo.ProgramId, programId,
codeStart, codeStart,
codePagesCount, codePagesCount,
(ProcessCreationFlags)meta.Flags | ProcessCreationFlags.IsApplication, (ProcessCreationFlags)meta.Flags | ProcessCreationFlags.IsApplication,
0, 0,
personalMmHeapPagesCount); personalMmHeapPagesCount);
context.Device.System.LibHacHorizonManager.InitializeApplicationClient(new ProgramId(programInfo.ProgramId), in npdm); context.Device.System.LibHacHorizonManager.InitializeApplicationClient(new ProgramId(programId), in npdm);
Result result; Result result;
KResourceLimit resourceLimit = new KResourceLimit(context); KResourceLimit resourceLimit = new(context);
long applicationRgSize = (long)context.MemoryManager.MemoryRegions[(int)MemoryRegion.Application].Size; long applicationRgSize = (long)context.MemoryManager.MemoryRegions[(int)MemoryRegion.Application].Size;
@ -293,26 +337,26 @@ namespace Ryujinx.HLE.HOS
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization failed setting resource limit values."); Logger.Error?.Print(LogClass.Loader, $"Process initialization failed setting resource limit values.");
return ProgramLoadResult.Failed; return ProcessResult.Failed;
} }
KProcess process = new KProcess(context, programInfo.AllowCodeMemoryForJit); KProcess process = new(context, allowCodeMemoryForJit);
MemoryRegion memoryRegion = (MemoryRegion)((npdm.Acid.Flags >> 2) & 0xf);
// NOTE: This field doesn't exists one firmware pre-5.0.0, a workaround have to be found.
MemoryRegion memoryRegion = (MemoryRegion)(npdm.Acid.Flags >> 2 & 0xf);
if (memoryRegion > MemoryRegion.NvServices) if (memoryRegion > MemoryRegion.NvServices)
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization failed due to invalid ACID flags."); Logger.Error?.Print(LogClass.Loader, $"Process initialization failed due to invalid ACID flags.");
return ProgramLoadResult.Failed; return ProcessResult.Failed;
} }
var processContextFactory = new ArmProcessContextFactory( var processContextFactory = new ArmProcessContextFactory(
context.Device.System.TickSource, context.Device.System.TickSource,
context.Device.Gpu, context.Device.Gpu,
programInfo.TitleIdText, $"{programId:x16}",
programInfo.DisplayVersion, applicationControlProperties.DisplayVersionString.ToString(),
programInfo.DiskCacheEnabled, diskCacheEnabled,
codeStart, codeStart,
codeSize); codeSize);
@ -327,7 +371,7 @@ namespace Ryujinx.HLE.HOS
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\"."); Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\".");
return ProgramLoadResult.Failed; return ProcessResult.Failed;
} }
for (int index = 0; index < executables.Length; index++) for (int index = 0; index < executables.Length; index++)
@ -335,32 +379,22 @@ namespace Ryujinx.HLE.HOS
Logger.Info?.Print(LogClass.Loader, $"Loading image {index} at 0x{nsoBase[index]:x16}..."); Logger.Info?.Print(LogClass.Loader, $"Loading image {index} at 0x{nsoBase[index]:x16}...");
result = LoadIntoMemory(process, executables[index], nsoBase[index]); result = LoadIntoMemory(process, executables[index], nsoBase[index]);
if (result != Result.Success) if (result != Result.Success)
{ {
Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\"."); Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\".");
return ProgramLoadResult.Failed; return ProcessResult.Failed;
} }
} }
process.DefaultCpuCore = meta.DefaultCpuId; process.DefaultCpuCore = meta.DefaultCpuId;
result = process.Start(meta.MainThreadPriority, meta.MainThreadStackSize);
if (result != Result.Success)
{
Logger.Error?.Print(LogClass.Loader, $"Process start returned error \"{result}\".");
return ProgramLoadResult.Failed;
}
context.Processes.TryAdd(process.Pid, process); context.Processes.TryAdd(process.Pid, process);
// Keep the build ids because the tamper machine uses them to know which process to associate a // Keep the build ids because the tamper machine uses them to know which process to associate a
// tamper to and also keep the starting address of each executable inside a process because some // tamper to and also keep the starting address of each executable inside a process because some
// memory modifications are relative to this address. // memory modifications are relative to this address.
ProcessTamperInfo tamperInfo = new ProcessTamperInfo( ProcessTamperInfo tamperInfo = new(
process, process,
buildIds, buildIds,
nsoBase, nsoBase,
@ -368,10 +402,13 @@ namespace Ryujinx.HLE.HOS
process.MemoryManager.AliasRegionStart, process.MemoryManager.AliasRegionStart,
process.MemoryManager.CodeRegionStart); process.MemoryManager.CodeRegionStart);
return new ProgramLoadResult(true, tamperInfo, processContextFactory.DiskCacheLoadState, process.Pid); // Once everything is loaded, we can load cheats.
device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(programId, tamperInfo, device.TamperMachine);
return new ProcessResult(metaLoader, applicationControlProperties, diskCacheEnabled, allowCodeMemoryForJit, processContextFactory.DiskCacheLoadState, process.Pid, meta.MainThreadPriority, meta.MainThreadStackSize);
} }
private static Result LoadIntoMemory(KProcess process, IExecutable image, ulong baseAddress) public static Result LoadIntoMemory(KProcess process, IExecutable image, ulong baseAddress)
{ {
ulong textStart = baseAddress + image.TextOffset; ulong textStart = baseAddress + image.TextOffset;
ulong roStart = baseAddress + image.RoOffset; ulong roStart = baseAddress + image.RoOffset;
@ -404,14 +441,12 @@ namespace Ryujinx.HLE.HOS
} }
Result result = SetProcessMemoryPermission(textStart, (ulong)image.Text.Length, KMemoryPermission.ReadAndExecute); Result result = SetProcessMemoryPermission(textStart, (ulong)image.Text.Length, KMemoryPermission.ReadAndExecute);
if (result != Result.Success) if (result != Result.Success)
{ {
return result; return result;
} }
result = SetProcessMemoryPermission(roStart, (ulong)image.Ro.Length, KMemoryPermission.Read); result = SetProcessMemoryPermission(roStart, (ulong)image.Ro.Length, KMemoryPermission.Read);
if (result != Result.Success) if (result != Result.Success)
{ {
return result; return result;

View file

@ -0,0 +1,92 @@
using LibHac.Loader;
using LibHac.Ns;
using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.Horizon.Common;
namespace Ryujinx.HLE.Loaders.Processes
{
public struct ProcessResult
{
public static ProcessResult Failed => new(null, new ApplicationControlProperty(), false, false, null, 0, 0, 0);
private readonly byte _mainThreadPriority;
private readonly uint _mainThreadStackSize;
public readonly IDiskCacheLoadState DiskCacheLoadState;
public readonly MetaLoader MetaLoader;
public readonly ApplicationControlProperty ApplicationControlProperties;
public readonly ulong ProcessId;
public string Name;
public ulong ProgramId;
public readonly string ProgramIdText;
public readonly bool Is64Bit;
public readonly bool DiskCacheEnabled;
public readonly bool AllowCodeMemoryForJit;
public ProcessResult(
MetaLoader metaLoader,
ApplicationControlProperty applicationControlProperties,
bool diskCacheEnabled,
bool allowCodeMemoryForJit,
IDiskCacheLoadState diskCacheLoadState,
ulong pid,
byte mainThreadPriority,
uint mainThreadStackSize)
{
_mainThreadPriority = mainThreadPriority;
_mainThreadStackSize = mainThreadStackSize;
DiskCacheLoadState = diskCacheLoadState;
ProcessId = pid;
MetaLoader = metaLoader;
ApplicationControlProperties = applicationControlProperties;
if (metaLoader is not null)
{
ulong programId = metaLoader.GetProgramId();
Name = metaLoader.GetProgramName();
ProgramId = programId;
ProgramIdText = $"{programId:x16}";
Is64Bit = metaLoader.IsProgram64Bit();
}
DiskCacheEnabled = diskCacheEnabled;
AllowCodeMemoryForJit = allowCodeMemoryForJit;
}
public bool Start(Switch device)
{
device.Configuration.ContentManager.LoadEntries(device);
Result result = device.System.KernelContext.Processes[ProcessId].Start(_mainThreadPriority, _mainThreadStackSize);
if (result != Result.Success)
{
Logger.Error?.Print(LogClass.Loader, $"Process start returned error \"{result}\".");
return false;
}
// TODO: LibHac npdm currently doesn't support version field.
string version;
if (ProgramId > 0x0100000000007FFF)
{
version = ApplicationControlProperties.DisplayVersionString.ToString();
}
else
{
version = device.System.ContentManager.GetCurrentFirmwareVersion().VersionString;
}
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]");
return true;
}
}
}

View file

@ -6,6 +6,7 @@ using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Apm; using Ryujinx.HLE.HOS.Services.Apm;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.HLE.Ui; using Ryujinx.HLE.Ui;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
@ -20,7 +21,7 @@ namespace Ryujinx.HLE
public GpuContext Gpu { get; } public GpuContext Gpu { get; }
public VirtualFileSystem FileSystem { get; } public VirtualFileSystem FileSystem { get; }
public HOS.Horizon System { get; } public HOS.Horizon System { get; }
public ApplicationLoader Application { get; } public ProcessLoader Processes { get; }
public PerformanceStatistics Statistics { get; } public PerformanceStatistics Statistics { get; }
public Hid Hid { get; } public Hid Hid { get; }
public TamperMachine TamperMachine { get; } public TamperMachine TamperMachine { get; }
@ -50,7 +51,7 @@ namespace Ryujinx.HLE
System = new HOS.Horizon(this); System = new HOS.Horizon(this);
Statistics = new PerformanceStatistics(); Statistics = new PerformanceStatistics();
Hid = new Hid(this, System.HidStorage); Hid = new Hid(this, System.HidStorage);
Application = new ApplicationLoader(this); Processes = new ProcessLoader(this);
TamperMachine = new TamperMachine(); TamperMachine = new TamperMachine();
System.State.SetLanguage(Configuration.SystemLanguage); System.State.SetLanguage(Configuration.SystemLanguage);
@ -64,29 +65,29 @@ namespace Ryujinx.HLE
System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode; System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode;
} }
public void LoadCart(string exeFsDir, string romFsFile = null) public bool LoadCart(string exeFsDir, string romFsFile = null)
{ {
Application.LoadCart(exeFsDir, romFsFile); return Processes.LoadUnpackedNca(exeFsDir, romFsFile);
} }
public void LoadXci(string xciFile) public bool LoadXci(string xciFile)
{ {
Application.LoadXci(xciFile); return Processes.LoadXci(xciFile);
} }
public void LoadNca(string ncaFile) public bool LoadNca(string ncaFile)
{ {
Application.LoadNca(ncaFile); return Processes.LoadNca(ncaFile);
} }
public void LoadNsp(string nspFile) public bool LoadNsp(string nspFile)
{ {
Application.LoadNsp(nspFile); return Processes.LoadNsp(nspFile);
} }
public void LoadProgram(string fileName) public bool LoadProgram(string fileName)
{ {
Application.LoadProgram(fileName); return Processes.LoadNxo(fileName);
} }
public bool WaitFifo() public bool WaitFifo()
@ -123,7 +124,7 @@ namespace Ryujinx.HLE
public void EnableCheats() public void EnableCheats()
{ {
FileSystem.ModLoader.EnableCheats(Application.TitleId, TamperMachine); FileSystem.ModLoader.EnableCheats(Processes.ActiveApplication.ProgramId, TamperMachine);
} }
public bool IsAudioMuted() public bool IsAudioMuted()
@ -152,4 +153,4 @@ namespace Ryujinx.HLE
} }
} }
} }
} }

View file

@ -447,10 +447,10 @@ namespace Ryujinx.Headless.SDL2
private static void SetupProgressHandler() private static void SetupProgressHandler()
{ {
if (_emulationContext.Application.DiskCacheLoadState != null) if (_emulationContext.Processes.ActiveApplication.DiskCacheLoadState != null)
{ {
_emulationContext.Application.DiskCacheLoadState.StateChanged -= ProgressHandler; _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged -= ProgressHandler;
_emulationContext.Application.DiskCacheLoadState.StateChanged += ProgressHandler; _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += ProgressHandler;
} }
_emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler; _emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler;
@ -608,12 +608,24 @@ namespace Ryujinx.Headless.SDL2
if (romFsFiles.Length > 0) if (romFsFiles.Length > 0)
{ {
Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS."); Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS.");
_emulationContext.LoadCart(path, romFsFiles[0]);
if (!_emulationContext.LoadCart(path, romFsFiles[0]))
{
_emulationContext.Dispose();
return false;
}
} }
else else
{ {
Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
_emulationContext.LoadCart(path);
if (!_emulationContext.LoadCart(path))
{
_emulationContext.Dispose();
return false;
}
} }
} }
else if (File.Exists(path)) else if (File.Exists(path))
@ -622,27 +634,52 @@ namespace Ryujinx.Headless.SDL2
{ {
case ".xci": case ".xci":
Logger.Info?.Print(LogClass.Application, "Loading as XCI."); Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
_emulationContext.LoadXci(path);
if (!_emulationContext.LoadXci(path))
{
_emulationContext.Dispose();
return false;
}
break; break;
case ".nca": case ".nca":
Logger.Info?.Print(LogClass.Application, "Loading as NCA."); Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
_emulationContext.LoadNca(path);
if (!_emulationContext.LoadNca(path))
{
_emulationContext.Dispose();
return false;
}
break; break;
case ".nsp": case ".nsp":
case ".pfs0": case ".pfs0":
Logger.Info?.Print(LogClass.Application, "Loading as NSP."); Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
_emulationContext.LoadNsp(path);
if (!_emulationContext.LoadNsp(path))
{
_emulationContext.Dispose();
return false;
}
break; break;
default: default:
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew."); Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
try try
{ {
_emulationContext.LoadProgram(path); if (!_emulationContext.LoadProgram(path))
{
_emulationContext.Dispose();
return false;
}
} }
catch (ArgumentOutOfRangeException) catch (ArgumentOutOfRangeException)
{ {
Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx."); Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
_emulationContext.Dispose();
return false; return false;
} }
break; break;
@ -664,4 +701,4 @@ namespace Ryujinx.Headless.SDL2
return true; return true;
} }
} }
} }

View file

@ -145,16 +145,14 @@ namespace Ryujinx.Headless.SDL2
private void InitializeWindow() private void InitializeWindow()
{ {
string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName) ? string.Empty var activeProcess = Device.Processes.ActiveApplication;
: $" - {Device.Application.TitleName}"; var nacp = activeProcess.ApplicationControlProperties;
int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
string titleVersionSection = string.IsNullOrWhiteSpace(Device.Application.DisplayVersion) ? string.Empty string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}";
: $" v{Device.Application.DisplayVersion}"; string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
string titleIdSection = string.IsNullOrWhiteSpace(Device.Application.TitleIdText) ? string.Empty string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
: $" ({Device.Application.TitleIdText.ToUpper()})";
string titleArchSection = Device.Application.TitleIs64Bit ? " (64-bit)" : " (32-bit)";
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, DefaultWidth, DefaultHeight, DefaultFlags | GetWindowFlags()); WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, DefaultWidth, DefaultHeight, DefaultFlags | GetWindowFlags());

View file

@ -11,12 +11,12 @@ using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.HLE.Loaders.Npdm;
using Ryujinx.Ui.Common.Configuration.System; using Ryujinx.Ui.Common.Configuration.System;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
@ -112,9 +112,9 @@ namespace Ryujinx.Ui.App.Common
{ {
return; return;
} }
string extension = Path.GetExtension(app).ToLower(); string extension = Path.GetExtension(app).ToLower();
if (!File.GetAttributes(app).HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso") if (!File.GetAttributes(app).HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso")
{ {
applications.Add(app); applications.Add(app);
@ -262,10 +262,9 @@ namespace Ryujinx.Ui.App.Common
controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new(); using MemoryStream stream = new();
icon.Get.AsStream().CopyTo(stream); icon.Get.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray(); applicationIcon = stream.ToArray();
if (applicationIcon != null) if (applicationIcon != null)
{ {
@ -400,7 +399,7 @@ namespace Ryujinx.Ui.App.Common
}); });
if (appMetadata.LastPlayed != "Never") if (appMetadata.LastPlayed != "Never")
{ {
if (!DateTime.TryParse(appMetadata.LastPlayed, out _)) if (!DateTime.TryParse(appMetadata.LastPlayed, out _))
{ {
Logger.Warning?.Print(LogClass.Application, $"Last played datetime \"{appMetadata.LastPlayed}\" is invalid for current system culture, skipping (did current culture change?)"); Logger.Warning?.Print(LogClass.Application, $"Last played datetime \"{appMetadata.LastPlayed}\" is invalid for current system culture, skipping (did current culture change?)");
@ -470,7 +469,7 @@ namespace Ryujinx.Ui.App.Common
private void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId) private void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId)
{ {
(_, _, Nca controlNca) = ApplicationLoader.GetGameData(_virtualFileSystem, pfs, 0); (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0);
// Return the ControlFS // Return the ControlFS
controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
@ -766,12 +765,12 @@ namespace Ryujinx.Ui.App.Common
private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs) private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs)
{ {
updatedControlFs = null; updatedControlFs = null;
string updatePath = "(unknown)"; string updatePath = "(unknown)";
try try
{ {
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath);
if (patchNca != null && controlNca != null) if (patchNca != null && controlNca != null)
{ {
@ -791,5 +790,119 @@ namespace Ryujinx.Ui.App.Common
return false; return false;
} }
public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex)
{
Nca mainNca = null;
Nca patchNca = null;
Nca controlNca = null;
fileSystem.ImportTickets(pfs);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(fileSystem.KeySet, ncaFile.Release().AsStorage());
int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
if (ncaProgramIndex != programIndex)
{
continue;
}
if (nca.Header.ContentType == NcaContentType.Program)
{
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{
patchNca = nca;
}
else
{
mainNca = nca;
}
}
else if (nca.Header.ContentType == NcaContentType.Control)
{
controlNca = nca;
}
}
return (mainNca, patchNca, controlNca);
}
public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex)
{
Nca patchNca = null;
Nca controlNca = null;
fileSystem.ImportTickets(pfs);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(fileSystem.KeySet, ncaFile.Release().AsStorage());
int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
if (ncaProgramIndex != programIndex)
{
continue;
}
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId)
{
break;
}
if (nca.Header.ContentType == NcaContentType.Program)
{
patchNca = nca;
}
else if (nca.Header.ContentType == NcaContentType.Control)
{
controlNca = nca;
}
}
return (patchNca, controlNca);
}
public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath)
{
updatePath = null;
if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
{
// Clear the program index part.
titleIdBase &= ~0xFUL;
// Load update information if exists.
string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
if (File.Exists(titleUpdateMetadataPath))
{
updatePath = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(titleUpdateMetadataPath).Selected;
if (File.Exists(updatePath))
{
FileStream file = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex);
}
}
}
return (null, null);
}
} }
} }

View file

@ -252,7 +252,7 @@ namespace Ryujinx
if (CommandLineState.LaunchPathArg != null) if (CommandLineState.LaunchPathArg != null)
{ {
mainWindow.LoadApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg); mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
} }
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))

View file

@ -590,10 +590,10 @@ namespace Ryujinx.Ui
private void SetupProgressUiHandlers() private void SetupProgressUiHandlers()
{ {
if (_emulationContext.Application.DiskCacheLoadState != null) if (_emulationContext.Processes.ActiveApplication.DiskCacheLoadState != null)
{ {
_emulationContext.Application.DiskCacheLoadState.StateChanged -= ProgressHandler; _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged -= ProgressHandler;
_emulationContext.Application.DiskCacheLoadState.StateChanged += ProgressHandler; _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += ProgressHandler;
} }
_emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler; _emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler;
@ -690,7 +690,111 @@ namespace Ryujinx.Ui
} }
} }
public void LoadApplication(string path, bool startFullscreen = false) private bool LoadApplication(string path, bool isFirmwareTitle)
{
SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
if (!SetupValidator.CanStartApplication(_contentManager, path, out UserError userError))
{
if (SetupValidator.CanFixStartApplication(_contentManager, path, userError, out firmwareVersion))
{
string message = $"Would you like to install the firmware embedded in this game? (Firmware {firmwareVersion.VersionString})";
ResponseType responseDialog = (ResponseType)GtkDialog.CreateConfirmationDialog("No Firmware Installed", message).Run();
if (responseDialog != ResponseType.Yes || !SetupValidator.TryFixStartApplication(_contentManager, path, userError, out _))
{
UserErrorDialog.CreateUserErrorDialog(userError);
return false;
}
// Tell the user that we installed a firmware for them.
firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
RefreshFirmwareLabel();
message = $"No installed firmware was found but Ryujinx was able to install firmware {firmwareVersion.VersionString} from the provided game.\nThe emulator will now start.";
GtkDialog.CreateInfoDialog($"Firmware {firmwareVersion.VersionString} was installed", message);
}
else
{
UserErrorDialog.CreateUserErrorDialog(userError);
return false;
}
}
Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}");
if (isFirmwareTitle)
{
Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA).");
return _emulationContext.LoadNca(path);
}
if (Directory.Exists(path))
{
string[] romFsFiles = Directory.GetFiles(path, "*.istorage");
if (romFsFiles.Length == 0)
{
romFsFiles = Directory.GetFiles(path, "*.romfs");
}
if (romFsFiles.Length > 0)
{
Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS.");
return _emulationContext.LoadCart(path, romFsFiles[0]);
}
Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
return _emulationContext.LoadCart(path);
}
if (File.Exists(path))
{
switch (System.IO.Path.GetExtension(path).ToLowerInvariant())
{
case ".xci":
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
return _emulationContext.LoadXci(path);
case ".nca":
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
return _emulationContext.LoadNca(path);
case ".nsp":
case ".pfs0":
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
return _emulationContext.LoadNsp(path);
default:
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
try
{
return _emulationContext.LoadProgram(path);
}
catch (ArgumentOutOfRangeException)
{
Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
return false;
}
}
}
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
return false;
}
public void RunApplication(string path, bool startFullscreen = false)
{ {
if (_gameLoaded) if (_gameLoaded)
{ {
@ -710,9 +814,6 @@ namespace Ryujinx.Ui
UpdateGraphicsConfig(); UpdateGraphicsConfig();
SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
bool isDirectory = Directory.Exists(path);
bool isFirmwareTitle = false; bool isFirmwareTitle = false;
if (path.StartsWith("@SystemContent")) if (path.StartsWith("@SystemContent"))
@ -722,124 +823,10 @@ namespace Ryujinx.Ui
isFirmwareTitle = true; isFirmwareTitle = true;
} }
if (!SetupValidator.CanStartApplication(_contentManager, path, out UserError userError)) if (!LoadApplication(path, isFirmwareTitle))
{ {
if (SetupValidator.CanFixStartApplication(_contentManager, path, userError, out firmwareVersion))
{
if (userError == UserError.NoFirmware)
{
string message = $"Would you like to install the firmware embedded in this game? (Firmware {firmwareVersion.VersionString})";
ResponseType responseDialog = (ResponseType)GtkDialog.CreateConfirmationDialog("No Firmware Installed", message).Run();
if (responseDialog != ResponseType.Yes)
{
UserErrorDialog.CreateUserErrorDialog(userError);
_emulationContext.Dispose();
SwitchToGameTable();
return;
}
}
if (!SetupValidator.TryFixStartApplication(_contentManager, path, userError, out _))
{
UserErrorDialog.CreateUserErrorDialog(userError);
_emulationContext.Dispose();
SwitchToGameTable();
return;
}
// Tell the user that we installed a firmware for them.
if (userError == UserError.NoFirmware)
{
firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
RefreshFirmwareLabel();
string message = $"No installed firmware was found but Ryujinx was able to install firmware {firmwareVersion.VersionString} from the provided game.\nThe emulator will now start.";
GtkDialog.CreateInfoDialog($"Firmware {firmwareVersion.VersionString} was installed", message);
}
}
else
{
UserErrorDialog.CreateUserErrorDialog(userError);
_emulationContext.Dispose();
SwitchToGameTable();
return;
}
}
Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}");
if (isFirmwareTitle)
{
Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA).");
_emulationContext.LoadNca(path);
}
else if (Directory.Exists(path))
{
string[] romFsFiles = Directory.GetFiles(path, "*.istorage");
if (romFsFiles.Length == 0)
{
romFsFiles = Directory.GetFiles(path, "*.romfs");
}
if (romFsFiles.Length > 0)
{
Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS.");
_emulationContext.LoadCart(path, romFsFiles[0]);
}
else
{
Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
_emulationContext.LoadCart(path);
}
}
else if (File.Exists(path))
{
switch (System.IO.Path.GetExtension(path).ToLowerInvariant())
{
case ".xci":
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
_emulationContext.LoadXci(path);
break;
case ".nca":
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
_emulationContext.LoadNca(path);
break;
case ".nsp":
case ".pfs0":
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
_emulationContext.LoadNsp(path);
break;
default:
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
try
{
_emulationContext.LoadProgram(path);
}
catch (ArgumentOutOfRangeException)
{
Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
}
break;
}
}
else
{
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
_emulationContext.Dispose(); _emulationContext.Dispose();
RendererWidget.Dispose(); SwitchToGameTable();
return; return;
} }
@ -852,10 +839,7 @@ namespace Ryujinx.Ui
Translator.IsReadyForTranslation.Reset(); Translator.IsReadyForTranslation.Reset();
Thread windowThread = new Thread(() => Thread windowThread = new(CreateGameWindow)
{
CreateGameWindow();
})
{ {
Name = "GUI.WindowThread" Name = "GUI.WindowThread"
}; };
@ -871,9 +855,10 @@ namespace Ryujinx.Ui
_firmwareInstallFile.Sensitive = false; _firmwareInstallFile.Sensitive = false;
_firmwareInstallDirectory.Sensitive = false; _firmwareInstallDirectory.Sensitive = false;
DiscordIntegrationModule.SwitchToPlayingState(_emulationContext.Application.TitleIdText, _emulationContext.Application.TitleName); DiscordIntegrationModule.SwitchToPlayingState(_emulationContext.Processes.ActiveApplication.ProgramIdText,
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString());
_applicationLibrary.LoadAndSaveMetaData(_emulationContext.Application.TitleIdText, appMetadata => _applicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata =>
{ {
appMetadata.LastPlayed = DateTime.UtcNow.ToString(); appMetadata.LastPlayed = DateTime.UtcNow.ToString();
}); });
@ -1055,7 +1040,7 @@ namespace Ryujinx.Ui
if (_emulationContext != null) if (_emulationContext != null)
{ {
UpdateGameMetadata(_emulationContext.Application.TitleIdText); UpdateGameMetadata(_emulationContext.Processes.ActiveApplication.ProgramIdText);
if (RendererWidget != null) if (RendererWidget != null)
{ {
@ -1174,7 +1159,7 @@ namespace Ryujinx.Ui
string path = (string)_tableStore.GetValue(treeIter, 9); string path = (string)_tableStore.GetValue(treeIter, 9);
LoadApplication(path); RunApplication(path);
} }
private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args) private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args)
@ -1260,7 +1245,7 @@ namespace Ryujinx.Ui
if (fileChooser.Run() == (int)ResponseType.Accept) if (fileChooser.Run() == (int)ResponseType.Accept)
{ {
LoadApplication(fileChooser.Filename); RunApplication(fileChooser.Filename);
} }
} }
} }
@ -1271,7 +1256,7 @@ namespace Ryujinx.Ui
{ {
if (fileChooser.Run() == (int)ResponseType.Accept) if (fileChooser.Run() == (int)ResponseType.Accept)
{ {
LoadApplication(fileChooser.Filename); RunApplication(fileChooser.Filename);
} }
} }
} }
@ -1287,7 +1272,7 @@ namespace Ryujinx.Ui
{ {
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
LoadApplication(contentPath); RunApplication(contentPath);
} }
private void Open_Ryu_Folder(object sender, EventArgs args) private void Open_Ryu_Folder(object sender, EventArgs args)
@ -1328,7 +1313,7 @@ namespace Ryujinx.Ui
{ {
if (_emulationContext != null) if (_emulationContext != null)
{ {
UpdateGameMetadata(_emulationContext.Application.TitleIdText); UpdateGameMetadata(_emulationContext.Processes.ActiveApplication.ProgramIdText);
} }
_pauseEmulation.Sensitive = false; _pauseEmulation.Sensitive = false;
@ -1533,7 +1518,7 @@ namespace Ryujinx.Ui
{ {
_userChannelPersistence.ShouldRestart = false; _userChannelPersistence.ShouldRestart = false;
LoadApplication(_currentEmulatedGamePath); RunApplication(_currentEmulatedGamePath);
} }
else else
{ {
@ -1596,7 +1581,9 @@ namespace Ryujinx.Ui
private void ManageCheats_Pressed(object sender, EventArgs args) private void ManageCheats_Pressed(object sender, EventArgs args)
{ {
var window = new CheatWindow(_virtualFileSystem, _emulationContext.Application.TitleId, _emulationContext.Application.TitleName); var window = new CheatWindow(_virtualFileSystem,
_emulationContext.Processes.ActiveApplication.ProgramId,
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString());
window.Destroyed += CheatWindow_Destroyed; window.Destroyed += CheatWindow_Destroyed;
window.Show(); window.Show();
@ -1639,7 +1626,7 @@ namespace Ryujinx.Ui
LastScannedAmiiboShowAll = _lastScannedAmiiboShowAll, LastScannedAmiiboShowAll = _lastScannedAmiiboShowAll,
LastScannedAmiiboId = _lastScannedAmiiboId, LastScannedAmiiboId = _lastScannedAmiiboId,
DeviceId = deviceId, DeviceId = deviceId,
TitleId = _emulationContext.Application.TitleIdText.ToUpper() TitleId = _emulationContext.Processes.ActiveApplication.ProgramIdText.ToUpper()
}; };
amiiboWindow.DeleteEvent += AmiiboWindow_DeleteEvent; amiiboWindow.DeleteEvent += AmiiboWindow_DeleteEvent;

View file

@ -495,16 +495,14 @@ namespace Ryujinx.Ui
{ {
parent.Present(); parent.Present();
string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName) ? string.Empty var activeProcess = Device.Processes.ActiveApplication;
: $" - {Device.Application.TitleName}"; var nacp = activeProcess.ApplicationControlProperties;
int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
string titleVersionSection = string.IsNullOrWhiteSpace(Device.Application.DisplayVersion) ? string.Empty string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}";
: $" v{Device.Application.DisplayVersion}"; string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
string titleIdSection = string.IsNullOrWhiteSpace(Device.Application.TitleIdText) ? string.Empty string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
: $" ({Device.Application.TitleIdText.ToUpper()})";
string titleArchSection = Device.Application.TitleIs64Bit ? " (64-bit)" : " (32-bit)";
parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}"; parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
}); });
@ -612,7 +610,7 @@ namespace Ryujinx.Ui
{ {
if (!ParentWindow.State.HasFlag(WindowState.Fullscreen)) if (!ParentWindow.State.HasFlag(WindowState.Fullscreen))
{ {
Device.Application.DiskCacheLoadState?.Cancel(); Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel();
} }
} }
}); });

View file

@ -15,6 +15,7 @@ using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using Ryujinx.Ui.Windows; using Ryujinx.Ui.Windows;
@ -260,7 +261,7 @@ namespace Ryujinx.Ui.Widgets
return; return;
} }
(Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
if (updatePatchNca != null) if (updatePatchNca != null)
{ {

View file

@ -9,6 +9,7 @@ using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.Ui.App.Common;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -94,7 +95,7 @@ namespace Ryujinx.Ui.Windows
try try
{ {
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0);
if (controlNca != null && patchNca != null) if (controlNca != null && patchNca != null)
{ {