nacp: centralize title logic in ApplicationControlProperty.Title property

This commit is contained in:
Mythrax 2026-02-24 19:46:51 +10:00
parent 772b3fb800
commit 7ff2874baa
No known key found for this signature in database
GPG key ID: 3D310DF18BB7ADB9
8 changed files with 100 additions and 150 deletions

View file

@ -88,30 +88,21 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
if (!isHomebrew && programId > 0x010000000000FFFF)
{
int langIdx = (int)device.System.State.DesiredTitleLanguage;
var decompressedTitles = NacpHelper.IsCompressed(in nacpData.Value)
? NacpHelper.DecompressTitleEntries(in nacpData.Value) : null;
int titleCount = decompressedTitles?.Length ?? 16;
var titles = NacpHelper.GetTitles(in nacpData.Value);
int titleCount = titles.Length;
if (langIdx < titleCount)
{
programName = decompressedTitles != null
? decompressedTitles[langIdx].NameString.ToString()
: nacpData.Value.Title[langIdx].NameString.ToString();
programName = titles[langIdx].NameString.ToString();
}
if (string.IsNullOrWhiteSpace(programName))
{
for (int i = 0; i < titleCount; i++)
{
bool empty = decompressedTitles != null
? decompressedTitles[i].Name[0] == 0
: nacpData.Value.Title[i].Name[0] == 0;
if (!empty)
if (titles[i].Name[0] != 0)
{
programName = decompressedTitles != null
? decompressedTitles[i].NameString.ToString()
: nacpData.Value.Title[i].NameString.ToString();
programName = titles[i].NameString.ToString();
break;
}
}

View file

@ -195,20 +195,15 @@ namespace Ryujinx.HLE.Loaders.Processes
nacpStorage.Read(0, nacpData.ByteSpan);
int langIdx = (int)_device.System.State.DesiredTitleLanguage;
var decompressedTitles = Utilities.NacpHelper.IsCompressed(in nacpData.Value)
? Utilities.NacpHelper.DecompressTitleEntries(in nacpData.Value) : null;
int titleCount = decompressedTitles?.Length ?? 16;
var titles = Utilities.NacpHelper.GetTitles(in nacpData.Value);
int titleCount = titles.Length;
if (langIdx < titleCount)
{
programName = decompressedTitles != null
? decompressedTitles[langIdx].NameString.ToString()
: nacpData.Value.Title[langIdx].NameString.ToString();
programName = titles[langIdx].NameString.ToString();
}
string verifyName = decompressedTitles != null
? decompressedTitles[(int)TitleLanguage.AmericanEnglish].NameString.ToString()
: nacpData.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString();
string verifyName = titles[(int)TitleLanguage.AmericanEnglish].NameString.ToString();
if ("Switch Verification" == verifyName)
throw new InvalidOperationException();
@ -216,15 +211,9 @@ namespace Ryujinx.HLE.Loaders.Processes
{
for (int i = 0; i < titleCount; i++)
{
bool empty = decompressedTitles != null
? decompressedTitles[i].Name[0] == 0
: nacpData.Value.Title[i].Name[0] == 0;
if (!empty)
if (titles[i].Name[0] != 0)
{
programName = decompressedTitles != null
? decompressedTitles[i].NameString.ToString()
: nacpData.Value.Title[i].NameString.ToString();
programName = titles[i].NameString.ToString();
break;
}
}

View file

@ -56,30 +56,21 @@ namespace Ryujinx.HLE.Loaders.Processes
ulong programId = metaLoader.ProgramId;
int langIdx = (int)titleLanguage;
var decompressedTitles = NacpHelper.IsCompressed(in ApplicationControlProperties)
? NacpHelper.DecompressTitleEntries(in ApplicationControlProperties) : null;
int titleCount = decompressedTitles?.Length ?? 16;
var titles = NacpHelper.GetTitles(in ApplicationControlProperties);
int titleCount = titles.Length;
if (langIdx < titleCount)
{
Name = decompressedTitles != null
? decompressedTitles[langIdx].NameString.ToString()
: ApplicationControlProperties.Title[langIdx].NameString.ToString();
Name = titles[langIdx].NameString.ToString();
}
if (string.IsNullOrWhiteSpace(Name))
{
for (int i = 0; i < titleCount; i++)
{
bool empty = decompressedTitles != null
? decompressedTitles[i].Name[0] == 0
: ApplicationControlProperties.Title[i].Name[0] == 0;
if (!empty)
if (titles[i].Name[0] != 0)
{
Name = decompressedTitles != null
? decompressedTitles[i].NameString.ToString()
: ApplicationControlProperties.Title[i].NameString.ToString();
Name = titles[i].NameString.ToString();
break;
}
}

View file

@ -1,61 +1,28 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using LibHac.Ns;
using Ryujinx.Horizon.Sdk.Ns;
namespace Ryujinx.HLE.Utilities
{
/// <summary>
/// Bridges from LibHac's NACP struct to title resolution. All title parsing logic
/// lives on <see cref="Ryujinx.Horizon.Sdk.Ns.ApplicationControlProperty.Title"/>; this helper
/// reinterprets the same bytes so call sites holding LibHac's struct can use it.
/// </summary>
public static class NacpHelper
{
private const int CompressedTitleCount = 32;
private const int UncompressedTitleCount = 16;
private const int TitleEntrySize = 0x300;
private const int TitleCompressionByteIndex = 1; // offset within Reserved3214
public static bool IsCompressed(ref readonly ApplicationControlProperty nacp)
=> nacp.Reserved3214[TitleCompressionByteIndex] == 1;
public static int GetTitleCount(ref readonly ApplicationControlProperty nacp)
=> IsCompressed(in nacp) ? CompressedTitleCount : UncompressedTitleCount;
/// <summary>
/// Decompresses a zlib-compressed NACP title block into 32 title entries.
/// Only call this when <see cref="IsCompressed"/> returns <c>true</c>.
/// Returns the resolved title entries (16 or 32 depending on compression).
/// Uses the same binary layout as Horizon.Sdk so we delegate to its property.
/// </summary>
public static ApplicationControlProperty.ApplicationTitle[] DecompressTitleEntries(
ref readonly ApplicationControlProperty nacp)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Ryujinx.Horizon.Sdk.Ns.ApplicationControlProperty.ApplicationTitle[] GetTitles(
in LibHac.Ns.ApplicationControlProperty nacp)
{
ReadOnlySpan<byte> titleBytes = MemoryMarshal.AsBytes(
(ReadOnlySpan<ApplicationControlProperty.ApplicationTitle>)nacp.Title);
ushort compressedBlobSize = BinaryPrimitives.ReadUInt16LittleEndian(titleBytes);
ReadOnlySpan<byte> compressedBlob = titleBytes.Slice(2, compressedBlobSize);
byte[] decompressed = new byte[CompressedTitleCount * TitleEntrySize];
using (var compressedStream = new MemoryStream(compressedBlob.ToArray()))
using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
{
int totalRead = 0;
while (totalRead < decompressed.Length)
{
int read = deflateStream.Read(decompressed, totalRead, decompressed.Length - totalRead);
if (read == 0)
break;
totalRead += read;
}
}
var result = new ApplicationControlProperty.ApplicationTitle[CompressedTitleCount];
for (int i = 0; i < CompressedTitleCount; i++)
{
result[i] = MemoryMarshal.Read<ApplicationControlProperty.ApplicationTitle>(
decompressed.AsSpan(i * TitleEntrySize, TitleEntrySize));
}
return result;
ref readonly var libHacRef = ref nacp;
ref var horizonRef = ref Unsafe.As<LibHac.Ns.ApplicationControlProperty, Ryujinx.Horizon.Sdk.Ns.ApplicationControlProperty>(
ref Unsafe.AsRef(in libHacRef));
return horizonRef.Title;
}
}
}

View file

@ -1,12 +1,16 @@
using Ryujinx.Common.Memory;
using System;
using System.Buffers.Binary;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
namespace Ryujinx.Horizon.Sdk.Ns
{
public struct ApplicationControlProperty
{
public Array16<ApplicationTitle> Title;
public Array16<ApplicationTitle> TitleBlock;
public Array37<byte> Isbn;
public StartupUserAccountValue StartupUserAccount;
public UserAccountSwitchLockValue UserAccountSwitchLock;
@ -78,6 +82,56 @@ namespace Ryujinx.Horizon.Sdk.Ns
public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0');
public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0');
private const int UncompressedTitleCount = 16;
private const int CompressedTitleCount = 32;
private const int TitleEntrySize = 0x300;
/// <summary>
/// Returns the resolved title entries. When <see cref="TitleCompression"/> is
/// <see cref="TitleCompressionValue.Enable"/>, the raw <see cref="TitleBlock"/> bytes are
/// decompressed (raw deflate) from 0x3000 into 0x6000 bytes yielding up to 32 entries.
/// Otherwise the 16 uncompressed entries from <see cref="TitleBlock"/> are returned directly.
/// </summary>
public readonly ApplicationTitle[] Title
{
get
{
if (TitleCompression != TitleCompressionValue.Enable)
{
var titles = new ApplicationTitle[UncompressedTitleCount];
for (int i = 0; i < UncompressedTitleCount; i++)
titles[i] = TitleBlock[i];
return titles;
}
ReadOnlySpan<byte> titleBytes = MemoryMarshal.AsBytes(TitleBlock.AsSpan());
ushort compressedBlobSize = BinaryPrimitives.ReadUInt16LittleEndian(titleBytes);
ReadOnlySpan<byte> compressedBlob = titleBytes.Slice(2, compressedBlobSize);
byte[] decompressed = new byte[CompressedTitleCount * TitleEntrySize];
using (var compressedStream = new MemoryStream(compressedBlob.ToArray()))
using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
{
int totalRead = 0;
while (totalRead < decompressed.Length)
{
int read = deflateStream.Read(decompressed, totalRead, decompressed.Length - totalRead);
if (read == 0)
break;
totalRead += read;
}
}
var result = new ApplicationTitle[CompressedTitleCount];
for (int i = 0; i < CompressedTitleCount; i++)
result[i] = MemoryMarshal.Read<ApplicationTitle>(
decompressed.AsSpan(i * TitleEntrySize, TitleEntrySize));
return result;
}
}
public struct ApplicationTitle
{
public ByteArray512 Name;

View file

@ -166,17 +166,8 @@ namespace Ryujinx.Headless
ProcessResult activeProcess = Device.Processes.ActiveApplication;
ApplicationControlProperty nacp = activeProcess.ApplicationControlProperties;
int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
string titleName;
if (Ryujinx.HLE.Utilities.NacpHelper.IsCompressed(in nacp))
{
var decompressed = Ryujinx.HLE.Utilities.NacpHelper.DecompressTitleEntries(in nacp);
titleName = desiredLanguage < decompressed.Length ? decompressed[desiredLanguage].NameString.ToString() : string.Empty;
}
else
{
titleName = desiredLanguage < 16 ? nacp.Title[desiredLanguage].NameString.ToString() : string.Empty;
}
var titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitles(in nacp);
string titleName = desiredLanguage < titles.Length ? titles[desiredLanguage].NameString.ToString() : string.Empty;
string titleNameSection = string.IsNullOrWhiteSpace(titleName) ? string.Empty : $" - {titleName}";
string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";

View file

@ -440,9 +440,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
GetApplicationInformation(ref controlHolder.Value, ref application);
string verifyName = NacpHelper.IsCompressed(in controlHolder.Value)
? NacpHelper.DecompressTitleEntries(in controlHolder.Value)[(int)TitleLanguage.AmericanEnglish].NameString.ToString()
: controlHolder.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString();
string verifyName = NacpHelper.GetTitles(in controlHolder.Value)[(int)TitleLanguage.AmericanEnglish].NameString.ToString();
if ("Switch Verification" == verifyName)
return false;
}
@ -1394,22 +1392,13 @@ namespace Ryujinx.Ava.Systems.AppLibrary
_ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
int langIdx = (int)desiredTitleLanguage;
var decompressedTitles = NacpHelper.IsCompressed(in controlData)
? NacpHelper.DecompressTitleEntries(in controlData) : null;
int titleCount = decompressedTitles?.Length ?? 16;
var titles = NacpHelper.GetTitles(in controlData);
int titleCount = titles.Length;
if (langIdx < titleCount)
{
if (decompressedTitles != null)
{
data.Name = decompressedTitles[langIdx].NameString.ToString();
data.Developer = decompressedTitles[langIdx].PublisherString.ToString();
}
else
{
data.Name = controlData.Title[langIdx].NameString.ToString();
data.Developer = controlData.Title[langIdx].PublisherString.ToString();
}
data.Name = titles[langIdx].NameString.ToString();
data.Developer = titles[langIdx].PublisherString.ToString();
}
else
{
@ -1421,16 +1410,9 @@ namespace Ryujinx.Ava.Systems.AppLibrary
{
for (int i = 0; i < titleCount; i++)
{
bool empty = decompressedTitles != null
? decompressedTitles[i].NameString.IsEmpty()
: controlData.Title[i].NameString.IsEmpty();
if (!empty)
if (!string.IsNullOrWhiteSpace(titles[i].NameString))
{
data.Name = decompressedTitles != null
? decompressedTitles[i].NameString.ToString()
: controlData.Title[i].NameString.ToString();
data.Name = titles[i].NameString.ToString();
break;
}
}
@ -1440,16 +1422,9 @@ namespace Ryujinx.Ava.Systems.AppLibrary
{
for (int i = 0; i < titleCount; i++)
{
bool empty = decompressedTitles != null
? decompressedTitles[i].PublisherString.IsEmpty()
: controlData.Title[i].PublisherString.IsEmpty();
if (!empty)
if (!string.IsNullOrWhiteSpace(titles[i].PublisherString))
{
data.Developer = decompressedTitles != null
? decompressedTitles[i].PublisherString.ToString()
: controlData.Title[i].PublisherString.ToString();
data.Developer = titles[i].PublisherString.ToString();
break;
}
}

View file

@ -170,16 +170,8 @@ namespace Ryujinx.Ava.UI.Views.Main
int langIdx = (int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage;
ref readonly ApplicationControlProperty nacp = ref ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties;
string name;
if (Ryujinx.HLE.Utilities.NacpHelper.IsCompressed(in nacp))
{
var decompressed = Ryujinx.HLE.Utilities.NacpHelper.DecompressTitleEntries(in nacp);
name = langIdx < decompressed.Length ? decompressed[langIdx].NameString.ToString() : string.Empty;
}
else
{
name = langIdx < 16 ? nacp.Title[langIdx].NameString.ToString() : string.Empty;
}
var titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitles(in nacp);
string name = langIdx < titles.Length ? titles[langIdx].NameString.ToString() : string.Empty;
await StyleableAppWindow.ShowAsync(
new CheatWindow(