optimise game menu for fw 20

- loading the control data is ran on its own thread, it does not block the main thread. allows for smooth scrolling like nintendos home menu.
- on fw20+, sphaira manually parses the control data, rather than using ns. manually parsing takes 20-40ms, which is faster than ms which can take 50-500ms.
- on fw19 and below, if the control data is not in ns cache, sphaira will manually parse the data as its twice as fast as ns. You can see how fast this is by loading the gamecard menu as that manually parses everything, and it loads the gamecard faster than the home menu
This commit is contained in:
ITotalJustice
2025-05-13 23:51:06 +01:00
parent b98ccb927e
commit 97d3fd396e
7 changed files with 363 additions and 124 deletions

View File

@@ -5,12 +5,15 @@
#include "fs.hpp"
#include "option.hpp"
#include <memory>
#include <vector>
namespace sphaira::ui::menu::game {
enum class NacpLoadStatus {
// not yet attempted to be loaded.
None,
// started loading.
Progress,
// loaded, ready to parse.
Loaded,
// failed to load, do not attempt to load again!
@@ -19,12 +22,11 @@ enum class NacpLoadStatus {
struct Entry {
u64 app_id{};
s64 size{};
char display_version[0x10]{};
NacpLanguageEntry lang{};
int image{};
std::unique_ptr<NsApplicationControlData> control{};
std::shared_ptr<NsApplicationControlData> control{};
u64 control_size{};
NacpLoadStatus status{NacpLoadStatus::None};
@@ -41,6 +43,38 @@ struct Entry {
}
};
struct ThreadResultData {
u64 id{};
std::shared_ptr<NsApplicationControlData> control{};
u64 control_size{};
char display_version[0x10]{};
NacpLanguageEntry lang{};
NacpLoadStatus status{NacpLoadStatus::None};
};
struct ThreadData {
ThreadData();
auto IsRunning() const -> bool;
void Run();
void Close();
void Push(u64 id);
void Push(std::span<const Entry> entries);
void Pop(std::vector<ThreadResultData>& out);
private:
UEvent m_uevent{};
Mutex m_mutex_id{};
Mutex m_mutex_result{};
// app_ids pushed to the queue, signal uevent when pushed.
std::vector<u64> m_ids{};
// control data pushed to the queue.
std::vector<ThreadResultData> m_result{};
std::atomic_bool m_running{};
};
enum SortType {
SortType_Updated,
};
@@ -79,6 +113,9 @@ private:
bool m_is_reversed{};
bool m_dirty{};
ThreadData m_thread_data{};
Thread m_thread{};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail};

View File

@@ -1,7 +1,10 @@
#pragma once
#include <switch.h>
#include "fs.hpp"
#include "keys.hpp"
#include "ncm.hpp"
#include <switch.h>
#include <vector>
namespace sphaira::nca {
@@ -216,6 +219,10 @@ Result DecryptKeak(const keys::Keys& keys, Header& header);
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
Result VerifyFixedKey(const Header& header);
// helpers that parse an nca.
Result ParseCnmt(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
auto GetKeyGenStr(u8 key_gen) -> const char*;
} // namespace sphaira::nca

View File

@@ -10,7 +10,6 @@
#include "fs.hpp"
#include "source/base.hpp"
#include "container/base.hpp"
#include "nx/ncm.hpp"
#include "ui/progress_box.hpp"
#include <memory>
#include <optional>
@@ -136,7 +135,4 @@ Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> so
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override = {});
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override = {});
Result ParseCnmtNca(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
Result ParseControlNca(const fs::FsPath& path, u64 program_id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
} // namespace sphaira::yati

View File

@@ -11,6 +11,7 @@
#include "defines.hpp"
#include "i18n.hpp"
#include "yati/nx/ncm.hpp"
#include "yati/nx/nca.hpp"
#include <utility>
#include <cstring>
@@ -19,20 +20,27 @@
namespace sphaira::ui::menu::game {
namespace {
// thank you Shchmue ^^
struct ApplicationOccupiedSizeEntry {
u8 storageId;
u8 padding[0x7];
u64 sizeApplication;
u64 sizePatch;
u64 sizeAddOnContent;
constexpr NcmStorageId NCM_STORAGE_IDS[]{
NcmStorageId_BuiltInUser,
NcmStorageId_SdCard,
};
struct ApplicationOccupiedSize {
ApplicationOccupiedSizeEntry entry[4];
};
NcmContentStorage ncm_cs[2];
NcmContentMetaDatabase ncm_db[2];
static_assert(sizeof(ApplicationOccupiedSize) == sizeof(NsApplicationOccupiedSize));
auto& GetNcmCs(u8 storage_id) {
if (storage_id == NcmStorageId_SdCard) {
return ncm_cs[1];
}
return ncm_cs[0];
}
auto& GetNcmDb(u8 storage_id) {
if (storage_id == NcmStorageId_SdCard) {
return ncm_db[1];
}
return ncm_db[0];
}
using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
@@ -48,19 +56,23 @@ Result Notify(Result rc, const std::string& error_message) {
return rc;
}
Result GetMetaEntries(const Entry& e, MetaEntries& out) {
Result GetMetaEntries(u64 id, MetaEntries& out) {
s32 count;
R_TRY(nsCountApplicationContentMeta(e.app_id, &count));
R_TRY(nsCountApplicationContentMeta(id, &count));
out.resize(count);
R_TRY(nsListApplicationContentMetaStatus(e.app_id, 0, out.data(), out.size(), &count));
R_TRY(nsListApplicationContentMetaStatus(id, 0, out.data(), out.size(), &count));
out.resize(count);
R_SUCCEED();
}
Result GetMetaEntries(const Entry& e, MetaEntries& out) {
return GetMetaEntries(e.app_id, out);
}
// also sets the status to error.
void FakeNacpEntry(Entry& e) {
void FakeNacpEntry(ThreadResultData& e) {
e.status = NacpLoadStatus::Error;
// fake the nacp entry
std::strcpy(e.lang.name, "Corrupted");
@@ -71,34 +83,113 @@ void FakeNacpEntry(Entry& e) {
bool LoadControlImage(Entry& e) {
if (!e.image && e.control) {
TimeStamp ts;
const auto jpeg_size = e.control_size - sizeof(NacpStruct);
e.image = nvgCreateImageMem(App::GetVg(), 0, e.control->icon, jpeg_size);
e.control.reset();
log_write("\t\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
return true;
}
return false;
}
Result LoadControlManual(u64 id, ThreadResultData& data) {
TimeStamp ts;
MetaEntries entries;
R_TRY(GetMetaEntries(id, entries));
R_UNLESS(!entries.empty(), 0x1);
const auto& ee = entries.back();
if (ee.storageID != NcmStorageId_SdCard && ee.storageID != NcmStorageId_BuiltInUser) {
return 0x1;
}
auto& db = GetNcmDb(ee.storageID);
auto& cs = GetNcmCs(ee.storageID);
NcmContentMetaKey key;
R_TRY(ncmContentMetaDatabaseGetLatestContentMetaKey(&db, &key, ee.application_id));
NcmContentId content_id;
R_TRY(ncmContentMetaDatabaseGetContentIdByType(&db, &content_id, &key, NcmContentType_Control));
u64 program_id;
R_TRY(ncmContentStorageGetProgramId(&cs, &program_id, &content_id, FsContentAttributes_All));
fs::FsPath path;
R_TRY(ncmContentStorageGetPath(&cs, path, sizeof(path), &content_id));
std::vector<u8> icon;
R_TRY(nca::ParseControl(path, program_id, &data.control->nacp, sizeof(data.control->nacp), &icon));
std::memcpy(data.control->icon, icon.data(), icon.size());
data.control_size = sizeof(data.control->nacp) + icon.size();
log_write("\t\t[manual control] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
R_SUCCEED();
}
auto LoadControlEntry(u64 id) -> ThreadResultData {
ThreadResultData data{};
data.id = id;
data.control = std::make_shared<NsApplicationControlData>();
data.status = NacpLoadStatus::Error;
bool manual_load = false;
if (hosversionBefore(20,0,0)) {
TimeStamp ts;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, data.control.get(), sizeof(NsApplicationControlData), &data.control_size))) {
manual_load = false;
log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
}
}
if (manual_load) {
manual_load = R_SUCCEEDED(LoadControlManual(id, data));
}
Result rc{};
if (!manual_load) {
TimeStamp ts;
rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, id, data.control.get(), sizeof(NsApplicationControlData), &data.control_size);
log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
}
if (R_SUCCEEDED(rc)) {
NacpLanguageEntry* lang{};
if (R_SUCCEEDED(rc = nsGetApplicationDesiredLanguage(&data.control->nacp, &lang)) && lang) {
data.lang = *lang;
std::memcpy(data.display_version, data.control->nacp.display_version, sizeof(data.display_version));
data.status = NacpLoadStatus::Loaded;
}
}
if (R_FAILED(rc)) {
FakeNacpEntry(data);
}
return data;
}
void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) {
e.status = result.status;
e.control = result.control;
e.control_size = result.control_size;
std::memcpy(e.display_version, result.display_version, sizeof(result.display_version));
e.lang = result.lang;
e.status = result.status;
}
void LoadControlEntry(Entry& e, bool force_image_load = false) {
if (e.status == NacpLoadStatus::None) {
e.control = std::make_unique<NsApplicationControlData>();
if (R_FAILED(nsGetApplicationControlData(NsApplicationControlSource_Storage, e.app_id, e.control.get(), sizeof(NsApplicationControlData), &e.control_size))) {
FakeNacpEntry(e);
} else {
NacpLanguageEntry* lang{};
if (R_FAILED(nsGetApplicationDesiredLanguage(&e.control->nacp, &lang)) || !lang) {
FakeNacpEntry(e);
} else {
e.lang = *lang;
std::memcpy(e.display_version, e.control->nacp.display_version, sizeof(e.display_version));
e.status = NacpLoadStatus::Loaded;
const auto result = LoadControlEntry(e.app_id);
LoadResultIntoEntry(e, result);
}
if (force_image_load) {
LoadControlImage(e);
}
}
}
if (force_image_load && e.status == NacpLoadStatus::Loaded) {
LoadControlImage(e);
}
}
@@ -112,8 +203,91 @@ void LaunchEntry(const Entry& e) {
Notify(rc, "Failed to launch application");
}
void ThreadFunc(void* user) {
auto data = static_cast<ThreadData*>(user);
while (data->IsRunning()) {
data->Run();
}
}
} // namespace
ThreadData::ThreadData() {
ueventCreate(&m_uevent, true);
mutexInit(&m_mutex_id);
mutexInit(&m_mutex_result);
m_running = true;
}
auto ThreadData::IsRunning() const -> bool {
return m_running;
}
void ThreadData::Run() {
while (IsRunning()) {
const auto waiter = waiterForUEvent(&m_uevent);
waitSingle(waiter, UINT64_MAX);
if (!IsRunning()) {
return;
}
std::vector<u64> ids;
{
mutexLock(&m_mutex_id);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex_id));
std::swap(ids, m_ids);
}
for (u64 i = 0; i < std::size(ids); i++) {
if (!IsRunning()) {
return;
}
// sleep after every other entry loaded.
if (i) {
svcSleepThread(1e+6*2); // 2ms
}
const auto result = LoadControlEntry(ids[i]);
mutexLock(&m_mutex_result);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex_result));
m_result.emplace_back(result);
}
}
}
void ThreadData::Close() {
m_running = false;
ueventSignal(&m_uevent);
}
void ThreadData::Push(u64 id) {
mutexLock(&m_mutex_id);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex_id));
const auto it = std::find(m_ids.begin(), m_ids.end(), id);
if (it == m_ids.end()) {
m_ids.emplace_back(id);
ueventSignal(&m_uevent);
}
}
void ThreadData::Push(std::span<const Entry> entries) {
for (auto& e : entries) {
Push(e.app_id);
}
}
void ThreadData::Pop(std::vector<ThreadResultData>& out) {
mutexLock(&m_mutex_result);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex_result));
std::swap(out, m_result);
m_result.clear();
}
Menu::Menu() : grid::Menu{"Games"_i18n} {
this->SetActions(
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
@@ -256,12 +430,30 @@ Menu::Menu() : grid::Menu{"Games"_i18n} {
nsInitialize();
nsGetApplicationRecordUpdateSystemEvent(&m_event);
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
ncmOpenContentMetaDatabase(std::addressof(ncm_db[i]), NCM_STORAGE_IDS[i]);
ncmOpenContentStorage(std::addressof(ncm_cs[i]), NCM_STORAGE_IDS[i]);
}
threadCreate(&m_thread, ThreadFunc, &m_thread_data, nullptr, 1024*32, 0x30, 1);
threadStart(&m_thread);
}
Menu::~Menu() {
m_thread_data.Close();
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
ncmContentMetaDatabaseClose(std::addressof(ncm_db[i]));
ncmContentStorageClose(std::addressof(ncm_cs[i]));
}
FreeEntries();
eventClose(&m_event);
nsExit();
threadWaitForExit(&m_thread);
threadClose(&m_thread);
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
@@ -293,12 +485,26 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
const int image_load_max = 2;
int image_load_count = 0;
std::vector<ThreadResultData> data;
m_thread_data.Pop(data);
for (const auto& d : data) {
const auto it = std::find_if(m_entries.begin(), m_entries.end(), [&d](auto& e) {
return e.app_id == d.id;
});
if (it != m_entries.end()) {
LoadResultIntoEntry(*it, d);
}
}
m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
// const auto& [x, y, w, h] = v;
auto& e = m_entries[pos];
if (e.status == NacpLoadStatus::None) {
LoadControlEntry(e);
m_thread_data.Push(e.app_id);
e.status = NacpLoadStatus::Progress;
}
// lazy load image
@@ -377,20 +583,7 @@ void Menu::ScanHomebrew() {
continue;
}
s64 size{};
// code for sorting by size, it's too slow however...
#if 0
ApplicationOccupiedSize occupied_size;
if (R_SUCCEEDED(nsCalculateApplicationOccupiedSize(e.application_id, (NsApplicationOccupiedSize*)&occupied_size))) {
for (auto& s : occupied_size.entry) {
size += s.sizeApplication;
size += s.sizePatch;
size += s.sizeAddOnContent;
}
}
#endif
m_entries.emplace_back(e.application_id, size);
m_entries.emplace_back(e.application_id);
}
offset += record_count;
@@ -401,6 +594,8 @@ void Menu::ScanHomebrew() {
log_write("games found: %zu time_taken: %.2f seconds %zu ms %zu ns\n", m_entries.size(), ts.GetSecondsD(), ts.GetMs(), ts.GetNs());
this->Sort();
SetIndex(0);
// m_thread_data.Push(m_entries);
}
void Menu::Sort() {

View File

@@ -320,7 +320,7 @@ Result Menu::GcMount() {
std::vector<u8> extended_header;
std::vector<NcmPackagedContentInfo> infos;
const auto path = BuildGcPath(e.name, &m_handle);
R_TRY(yati::ParseCnmtNca(path, 0, header, extended_header, infos));
R_TRY(nca::ParseCnmt(path, 0, header, extended_header, infos));
u8 key_gen;
FsRightsId rights_id;
@@ -511,7 +511,10 @@ void Menu::OnChangeIndex(s64 new_index) {
fsGetProgramId(&program_id, path, FsContentAttributes_All);
}
if (R_SUCCEEDED(yati::ParseControlNca(path, program_id, &nacp, sizeof(nacp), &icon))) {
TimeStamp ts;
if (R_SUCCEEDED(nca::ParseControl(path, program_id, &nacp, sizeof(nacp), &icon))) {
log_write("\t\tnca::ParseControl(): %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
log_write("managed to parse control nca %s\n", path.s);
NacpLanguageEntry* lang_entry{};
nacpGetLanguageEntry(&nacp, &lang_entry);

View File

@@ -151,6 +151,74 @@ Result VerifyFixedKey(const Header& header) {
R_SUCCEED();
}
Result ParseCnmt(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos) {
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentMeta, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
FsDir dir;
R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 total_entries;
FsDirectoryEntry buf;
R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf)));
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 offset{};
u64 bytes_read;
R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read extended header
extended_header.resize(header.meta_header.extended_header_size);
R_TRY(fsFileRead(std::addressof(file), offset, extended_header.data(), extended_header.size(), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read infos.
infos.resize(header.meta_header.content_count);
R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read)));
offset += bytes_read;
R_SUCCEED();
}
Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out, s64 nacp_size, std::vector<u8>* icon_out) {
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentControl, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
// read nacp.
if (nacp_out) {
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, nacp_out, nacp_size, 0, &bytes_read));
}
// read icon.
if (icon_out) {
// todo: use matching icon based on the language version.
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/icon_AmericanEnglish.dat"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 size;
R_TRY(fsFileGetSize(std::addressof(file), std::addressof(size)));
icon_out->resize(size);
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, icon_out->data(), icon_out->size(), 0, &bytes_read));
}
R_SUCCEED();
}
auto GetKeyGenStr(u8 key_gen) -> const char* {
switch (key_gen) {
case KeyGenerationOld_100: return "1.0.0";

View File

@@ -929,7 +929,7 @@ Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
} else if (nca.header.content_type == nca::ContentType_Control) {
NacpLanguageEntry entry;
std::vector<u8> icon;
R_TRY(yati::ParseControlNca(path, nca.header.program_id, &entry, sizeof(entry), &icon));
R_TRY(nca::ParseControl(path, nca.header.program_id, &entry, sizeof(entry), &icon));
pbox->SetTitle(entry.name).SetImageData(icon);
}
@@ -949,7 +949,7 @@ Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cn
ncm::PackagedContentMeta header;
std::vector<NcmPackagedContentInfo> infos;
R_TRY(ParseCnmtNca(path, cnmt.header.program_id, header, cnmt.extended_header, infos));
R_TRY(nca::ParseCnmt(path, cnmt.header.program_id, header, cnmt.extended_header, infos));
for (const auto& packed_info : infos) {
const auto& info = packed_info.info;
@@ -1418,71 +1418,4 @@ Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Bas
}
}
Result ParseCnmtNca(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos) {
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentMeta, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
FsDir dir;
R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 total_entries;
FsDirectoryEntry buf;
R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf)));
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 offset{};
u64 bytes_read;
R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read extended header
extended_header.resize(header.meta_header.extended_header_size);
R_TRY(fsFileRead(std::addressof(file), offset, extended_header.data(), extended_header.size(), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read infos.
infos.resize(header.meta_header.content_count);
R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read)));
offset += bytes_read;
R_SUCCEED();
}
Result ParseControlNca(const fs::FsPath& path, u64 program_id, void* nacp_out, s64 nacp_size, std::vector<u8>* icon_out) {
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentControl, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
// read nacp.
if (nacp_out) {
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, nacp_out, nacp_size, 0, &bytes_read));
}
// read icon.
if (icon_out) {
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/icon_AmericanEnglish.dat"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 size;
R_TRY(fsFileGetSize(std::addressof(file), std::addressof(size)));
icon_out->resize(size);
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, icon_out->data(), icon_out->size(), 0, &bytes_read));
}
R_SUCCEED();
}
} // namespace sphaira::yati