simplify title cache loading.

This commit is contained in:
ITotalJustice
2025-06-21 15:32:55 +01:00
parent 4d27bf5492
commit 1f7179e941
9 changed files with 714 additions and 1015 deletions

View File

@@ -83,6 +83,7 @@ add_executable(sphaira
source/ftpsrv_helper.cpp
source/haze_helper.cpp
source/threaded_file_transfer.cpp
source/title_info.cpp
source/minizip_helper.cpp
source/usb/base.cpp

View File

@@ -0,0 +1,96 @@
#pragma once
// #include <optional>
// #include <variant>
// #include <list>
// #include <string>
#include "fs.hpp"
#include <optional>
#include <span>
#include <vector>
#include <memory>
#include <functional>
#include <switch.h>
namespace sphaira::title {
constexpr u32 ContentMetaTypeToContentFlag(u8 meta_type) {
if (meta_type & 0x80) {
return 1 << (meta_type - 0x80);
}
return 0;
}
enum ContentFlag {
ContentFlag_Application = ContentMetaTypeToContentFlag(NcmContentMetaType_Application),
ContentFlag_Patch = ContentMetaTypeToContentFlag(NcmContentMetaType_Patch),
ContentFlag_AddOnContent = ContentMetaTypeToContentFlag(NcmContentMetaType_AddOnContent),
ContentFlag_DataPatch = ContentMetaTypeToContentFlag(NcmContentMetaType_DataPatch),
// nca locations where a control.nacp can exist.
ContentFlag_Nacp = ContentFlag_Application | ContentFlag_Patch,
// all of the above.
ContentFlag_All = ContentFlag_Application | ContentFlag_Patch | ContentFlag_AddOnContent | ContentFlag_DataPatch,
};
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!
Error,
};
struct ThreadResultData {
u64 id{};
std::shared_ptr<NsApplicationControlData> control{};
u64 jpeg_size{};
NacpLanguageEntry lang{};
NacpLoadStatus status{NacpLoadStatus::None};
};
using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
// starts background thread.
Result Init();
void Exit();
// adds new entry to queue.
void Push(u64 app_id);
// adds array of entries to queue.
void Push(std::span<const u64> app_ids);
#if 0
// removes entry from the queue into out.
void Pop(u64 app_id, std::vector<ThreadResultData>& out);
// removes array of entries from the queue into out.
void Pop(std::span<const u64> app_ids, std::vector<ThreadResultData>& out);
// removes all entries from the queue into out.
void Pop(std::vector<ThreadResultData>& out);
#endif
// gets entry without removing it from the queue.
auto Get(u64 app_id) -> std::optional<ThreadResultData>;
// gets array of entries without removing it from the queue.
void Get(std::span<const u64> app_ids, std::vector<ThreadResultData>& out);
auto GetNcmCs(u8 storage_id) -> NcmContentStorage&;
auto GetNcmDb(u8 storage_id) -> NcmContentMetaDatabase&;
// gets all meta entries for an id.
Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags = ContentFlag_All);
// returns the nca path of a control nca.
Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path);
// single threaded title info fetch.
auto LoadControlEntry(u64 id, bool* cached = nullptr) -> ThreadResultData;
// taken from nxdumptool.
void utilsReplaceIllegalCharacters(char *str, bool ascii_only);
} // namespace sphaira::title

View File

@@ -2,6 +2,7 @@
#include "ui/menus/grid_menu_base.hpp"
#include "ui/list.hpp"
#include "title_info.hpp"
#include "fs.hpp"
#include "option.hpp"
#include <memory>
@@ -10,17 +11,6 @@
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!
Error,
};
struct Entry {
u64 app_id{};
NacpLanguageEntry lang{};
@@ -29,7 +19,7 @@ struct Entry {
std::shared_ptr<NsApplicationControlData> control{};
u64 jpeg_size{};
NacpLoadStatus status{NacpLoadStatus::None};
title::NacpLoadStatus status{title::NacpLoadStatus::None};
auto GetName() const -> const char* {
return lang.name;
@@ -40,45 +30,6 @@ struct Entry {
}
};
struct ThreadResultData {
u64 id{};
std::shared_ptr<NsApplicationControlData> control{};
u64 jpeg_size{};
NacpLanguageEntry lang{};
NacpLoadStatus status{NacpLoadStatus::None};
};
struct ThreadData {
ThreadData(bool title_cache);
void Run();
void Close();
void Push(u64 id);
void Push(std::span<const Entry> entries);
void Pop(std::vector<ThreadResultData>& out);
auto IsRunning() const -> bool {
return m_running;
}
auto IsTitleCacheEnabled() const {
return m_title_cache;
}
private:
UEvent m_uevent{};
Mutex m_mutex_id{};
Mutex m_mutex_result{};
bool m_title_cache{};
// 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,
};
@@ -144,9 +95,6 @@ private:
bool m_is_reversed{};
bool m_dirty{};
std::unique_ptr<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_Grid};

View File

@@ -151,8 +151,8 @@ struct ApplicationEntry {
u64 app_id{};
u32 version{};
u8 key_gen{};
std::unique_ptr<NsApplicationControlData> control{};
u64 control_size{};
std::shared_ptr<NsApplicationControlData> control{};
u64 jpeg_size{};
NacpLanguageEntry lang_entry{};
std::vector<GcCollections> application{};

View File

@@ -2,6 +2,7 @@
#include "ui/menus/grid_menu_base.hpp"
#include "ui/list.hpp"
#include "title_info.hpp"
#include "fs.hpp"
#include "option.hpp"
#include "dumper.hpp"
@@ -11,17 +12,6 @@
namespace sphaira::ui::menu::save {
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!
Error,
};
struct Entry final : FsSaveDataInfo {
NacpLanguageEntry lang{};
int image{};
@@ -29,7 +19,7 @@ struct Entry final : FsSaveDataInfo {
std::shared_ptr<NsApplicationControlData> control{};
u64 jpeg_size{};
NacpLoadStatus status{NacpLoadStatus::None};
title::NacpLoadStatus status{title::NacpLoadStatus::None};
auto GetName() const -> const char* {
return lang.name;
@@ -40,37 +30,6 @@ struct Entry final : FsSaveDataInfo {
}
};
struct ThreadResultData {
u64 id{};
std::shared_ptr<NsApplicationControlData> control{};
u64 jpeg_size{};
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,
};
@@ -145,9 +104,6 @@ private:
s64 m_account_index{};
u8 m_data_type{FsSaveDataType_Account};
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_Grid};

View File

@@ -0,0 +1,557 @@
#include "title_info.hpp"
#include "defines.hpp"
#include "ui/types.hpp"
#include "log.hpp"
#include "yati/nx/nca.hpp"
#include "yati/nx/ncm.hpp"
#include <cstring>
#include <atomic>
#include <ranges>
#include <algorithm>
#include <nxtc.h>
namespace sphaira::title {
namespace {
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
constexpr int THREAD_CORE = 1;
struct ThreadData {
ThreadData(bool title_cache);
void Run();
void Close();
void Push(u64 id);
void Push(std::span<const u64> app_ids);
#if 0
auto Pop(u64 app_id) -> std::optional<ThreadResultData>;
void Pop(std::span<const u64> app_ids, std::vector<ThreadResultData>& out);
void PopAll(std::vector<ThreadResultData>& out);
#endif
auto Get(u64 app_id) -> std::optional<ThreadResultData>;
void Get(std::span<const u64> app_ids, std::vector<ThreadResultData>& out);
auto IsRunning() const -> bool {
return m_running;
}
auto IsTitleCacheEnabled() const {
return m_title_cache;
}
private:
UEvent m_uevent{};
Mutex m_mutex_id{};
Mutex m_mutex_result{};
bool m_title_cache{};
// 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{};
};
Mutex g_mutex{};
Thread g_thread{};
u32 g_ref_count{};
std::unique_ptr<ThreadData> g_thread_data{};
struct NcmEntry {
const NcmStorageId storage_id;
NcmContentStorage cs{};
NcmContentMetaDatabase db{};
void Open() {
if (R_FAILED(ncmOpenContentMetaDatabase(std::addressof(db), storage_id))) {
log_write("\tncmOpenContentMetaDatabase() failed. storage_id: %u\n", storage_id);
} else {
log_write("\tncmOpenContentMetaDatabase() success. storage_id: %u\n", storage_id);
}
if (R_FAILED(ncmOpenContentStorage(std::addressof(cs), storage_id))) {
log_write("\tncmOpenContentStorage() failed. storage_id: %u\n", storage_id);
} else {
log_write("\tncmOpenContentStorage() success. storage_id: %u\n", storage_id);
}
}
void Close() {
ncmContentMetaDatabaseClose(std::addressof(db));
ncmContentStorageClose(std::addressof(cs));
db = {};
cs = {};
}
};
constinit NcmEntry ncm_entries[] = {
// on memory, will become invalid on the gamecard being inserted / removed.
{ NcmStorageId_GameCard },
// normal (save), will remain valid.
{ NcmStorageId_BuiltInUser },
{ NcmStorageId_SdCard },
};
auto& GetNcmEntry(u8 storage_id) {
auto it = std::ranges::find_if(ncm_entries, [storage_id](auto& e){
return storage_id == e.storage_id;
});
if (it == std::end(ncm_entries)) {
log_write("unable to find valid ncm entry: %u\n", storage_id);
return ncm_entries[0];
}
return *it;
}
// taken from nxtc
constexpr u8 g_nacpLangTable[SetLanguage_Total] = {
[SetLanguage_JA] = 2,
[SetLanguage_ENUS] = 0,
[SetLanguage_FR] = 3,
[SetLanguage_DE] = 4,
[SetLanguage_IT] = 7,
[SetLanguage_ES] = 6,
[SetLanguage_ZHCN] = 14,
[SetLanguage_KO] = 12,
[SetLanguage_NL] = 8,
[SetLanguage_PT] = 10,
[SetLanguage_RU] = 11,
[SetLanguage_ZHTW] = 13,
[SetLanguage_ENGB] = 1,
[SetLanguage_FRCA] = 9,
[SetLanguage_ES419] = 5,
[SetLanguage_ZHHANS] = 14,
[SetLanguage_ZHHANT] = 13,
[SetLanguage_PTBR] = 15
};
auto GetNacpLangEntryIndex() -> u8 {
SetLanguage lang{SetLanguage_ENUS};
nxtcGetCacheLanguage(&lang);
return g_nacpLangTable[lang];
}
// also sets the status to error.
void FakeNacpEntry(ThreadResultData& e) {
e.status = NacpLoadStatus::Error;
// fake the nacp entry
std::strcpy(e.lang.name, "Corrupted");
std::strcpy(e.lang.author, "Corrupted");
e.control.reset();
}
Result LoadControlManual(u64 id, ThreadResultData& data) {
TimeStamp ts;
MetaEntries entries;
R_TRY(GetMetaEntries(id, entries, ContentFlag_Nacp));
R_UNLESS(!entries.empty(), Result_GameEmptyMetaEntries);
u64 program_id;
fs::FsPath path;
R_TRY(GetControlPathFromStatus(entries.back(), &program_id, &path));
std::vector<u8> icon;
R_TRY(nca::ParseControl(path, program_id, &data.control->nacp.lang[GetNacpLangEntryIndex()], sizeof(NacpLanguageEntry), &icon));
std::memcpy(data.control->icon, icon.data(), icon.size());
data.jpeg_size = icon.size();
log_write("\t\t[manual control] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
R_SUCCEED();
}
ThreadData::ThreadData(bool title_cache) : m_title_cache{title_cache} {
ueventCreate(&m_uevent, true);
mutexInit(&m_mutex_id);
mutexInit(&m_mutex_result);
m_running = true;
}
void ThreadData::Run() {
const auto waiter = waiterForUEvent(&m_uevent);
while (IsRunning()) {
const auto rc = waitSingle(waiter, 3e+9);
// if we timed out, flush the cache and poll again.
if (R_FAILED(rc)) {
nxtcFlushCacheFile();
continue;
}
if (!IsRunning()) {
return;
}
std::vector<u64> ids;
{
SCOPED_MUTEX(&m_mutex_id);
std::swap(ids, m_ids);
}
for (u64 i = 0; i < std::size(ids); i++) {
if (!IsRunning()) {
return;
}
bool cached{};
const auto result = LoadControlEntry(ids[i], &cached);
if (!cached) {
// sleep after every other entry loaded.
svcSleepThread(2e+6); // 2ms
}
SCOPED_MUTEX(&m_mutex_result);
m_result.emplace_back(result);
}
}
}
void ThreadData::Close() {
m_running = false;
ueventSignal(&m_uevent);
}
void ThreadData::Push(u64 id) {
SCOPED_MUTEX(&m_mutex_id);
SCOPED_MUTEX(&m_mutex_result);
const auto it_id = std::ranges::find(m_ids, id);
const auto it_result = std::ranges::find_if(m_result, [id](auto& e){
return id == e.id;
});
if (it_id == m_ids.end() && it_result == m_result.end()) {
m_ids.emplace_back(id);
ueventSignal(&m_uevent);
}
}
void ThreadData::Push(std::span<const u64> app_ids) {
for (auto& e : app_ids) {
Push(e);
}
}
#if 0
auto ThreadData::Pop(u64 app_id) -> std::optional<ThreadResultData> {
SCOPED_MUTEX(&m_mutex_result);
for (s64 i = 0; i < std::size(m_result); i++) {
if (app_id == m_result[i].id) {
const auto result = m_result[i];
m_result.erase(m_result.begin() + i);
return result;
}
}
return std::nullopt;
}
void ThreadData::Pop(std::span<const u64> app_ids, std::vector<ThreadResultData>& out) {
for (auto& e : app_ids) {
if (const auto result = Pop(e)) {
out.emplace_back(*result);
}
}
}
void ThreadData::PopAll(std::vector<ThreadResultData>& out) {
SCOPED_MUTEX(&m_mutex_result);
std::swap(out, m_result);
m_result.clear();
}
#endif
auto ThreadData::Get(u64 app_id) -> std::optional<ThreadResultData> {
SCOPED_MUTEX(&m_mutex_result);
for (s64 i = 0; i < std::size(m_result); i++) {
if (app_id == m_result[i].id) {
return m_result[i];
}
}
return std::nullopt;
}
void ThreadData::Get(std::span<const u64> app_ids, std::vector<ThreadResultData>& out) {
for (auto& e : app_ids) {
if (const auto result = Get(e)) {
out.emplace_back(*result);
}
}
}
void ThreadFunc(void* user) {
auto data = static_cast<ThreadData*>(user);
if (data->IsTitleCacheEnabled() && !nxtcInitialize()) {
log_write("[NXTC] failed to init cache\n");
}
ON_SCOPE_EXIT(nxtcExit());
while (data->IsRunning()) {
data->Run();
}
}
} // namespace
// starts background thread.
Result Init() {
SCOPED_MUTEX(&g_mutex);
if (g_ref_count) {
R_SUCCEED();
}
if (!g_ref_count) {
R_TRY(nsInitialize());
R_TRY(ncmInitialize());
for (auto& e : ncm_entries) {
e.Open();
}
g_thread_data = std::make_unique<ThreadData>(true);
R_TRY(threadCreate(&g_thread, ThreadFunc, g_thread_data.get(), nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
svcSetThreadCoreMask(g_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE));
R_TRY(threadStart(&g_thread));
}
g_ref_count++;
R_SUCCEED();
}
void Exit() {
SCOPED_MUTEX(&g_mutex);
if (!g_ref_count) {
return;
}
g_ref_count--;
if (!g_ref_count) {
g_thread_data->Close();
for (auto& e : ncm_entries) {
e.Close();
}
threadWaitForExit(&g_thread);
threadClose(&g_thread);
g_thread_data.reset();
nsExit();
ncmExit();
}
}
// adds new entry to queue.
void Push(u64 app_id) {
SCOPED_MUTEX(&g_mutex);
if (g_thread_data) {
g_thread_data->Push(app_id);
}
}
// adds array of entries to queue.
void Push(std::span<const u64> app_ids) {
SCOPED_MUTEX(&g_mutex);
if (g_thread_data) {
g_thread_data->Push(app_ids);
}
}
// gets entry without removing it from the queue.
auto Get(u64 app_id) -> std::optional<ThreadResultData> {
SCOPED_MUTEX(&g_mutex);
if (g_thread_data) {
return g_thread_data->Get(app_id);
}
return {};
}
// gets array of entries without removing it from the queue.
void Get(std::span<const u64> app_ids, std::vector<ThreadResultData>& out) {
SCOPED_MUTEX(&g_mutex);
if (g_thread_data) {
g_thread_data->Get(app_ids, out);
}
}
auto GetNcmCs(u8 storage_id) -> NcmContentStorage& {
return GetNcmEntry(storage_id).cs;
}
auto GetNcmDb(u8 storage_id) -> NcmContentMetaDatabase& {
return GetNcmEntry(storage_id).db;
}
Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags) {
for (s32 i = 0; ; i++) {
s32 count;
NsApplicationContentMetaStatus status;
R_TRY(nsListApplicationContentMetaStatus(id, i, &status, 1, &count));
if (!count) {
break;
}
if (flags & ContentMetaTypeToContentFlag(status.meta_type)) {
out.emplace_back(status);
}
}
R_SUCCEED();
}
Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path) {
const auto& ee = status;
if (ee.storageID != NcmStorageId_SdCard && ee.storageID != NcmStorageId_BuiltInUser && ee.storageID != NcmStorageId_GameCard) {
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));
R_TRY(ncmContentStorageGetProgramId(&cs, out_program_id, &content_id, FsContentAttributes_All));
R_TRY(ncmContentStorageGetPath(&cs, out_path->s, sizeof(*out_path), &content_id));
R_SUCCEED();
}
auto LoadControlEntry(u64 id, bool* cached) -> ThreadResultData {
// try and fetch from results first, before manually loading.
if (auto data = Get(id)) {
return *data;
}
TimeStamp ts;
ThreadResultData result{id};
result.control = std::make_shared<NsApplicationControlData>();
result.status = NacpLoadStatus::Error;
if (auto data = nxtcGetApplicationMetadataEntryById(id)) {
log_write("[NXTC] loaded from cache time taken: %.2fs %zums %zuns\n", ts.GetSecondsD(), ts.GetMs(), ts.GetNs());
ON_SCOPE_EXIT(nxtcFreeApplicationMetadata(&data));
if (cached) {
*cached = true;
}
result.status = NacpLoadStatus::Loaded;
std::strcpy(result.lang.name, data->name);
std::strcpy(result.lang.author, data->publisher);
std::memcpy(result.control->icon, data->icon_data, data->icon_size);
result.jpeg_size = data->icon_size;
} else {
if (cached) {
*cached = false;
}
bool manual_load = true;
if (hosversionBefore(20,0,0)) {
TimeStamp ts;
u64 actual_size;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, result.control.get(), sizeof(NsApplicationControlData), &actual_size))) {
manual_load = false;
result.jpeg_size = actual_size - sizeof(NacpStruct);
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, result));
}
Result rc{};
if (!manual_load) {
TimeStamp ts;
u64 actual_size;
if (R_SUCCEEDED(rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, id, result.control.get(), sizeof(NsApplicationControlData), &actual_size))) {
result.jpeg_size = actual_size - sizeof(NacpStruct);
log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
}
}
if (R_FAILED(rc)) {
FakeNacpEntry(result);
} else {
if (!manual_load) {
NacpLanguageEntry* lang;
if (R_SUCCEEDED(nsGetApplicationDesiredLanguage(&result.control->nacp, &lang))) {
result.lang = *lang;
}
} else {
result.lang = result.control->nacp.lang[GetNacpLangEntryIndex()];
}
nxtcAddEntry(id, &result.control->nacp, result.jpeg_size, result.control->icon, true);
result.status = NacpLoadStatus::Loaded;
}
}
return result;
}
// taken from nxdumptool.
void utilsReplaceIllegalCharacters(char *str, bool ascii_only)
{
static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|";
size_t str_size = 0, cur_pos = 0;
if (!str || !(str_size = strlen(str))) return;
u8 *ptr1 = (u8*)str, *ptr2 = ptr1;
ssize_t units = 0;
u32 code = 0;
bool repl = false;
while(cur_pos < str_size)
{
units = decode_utf8(&code, ptr1);
if (units < 0) break;
if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \
(units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars))))
{
if (!repl)
{
*ptr2++ = '_';
repl = true;
}
} else {
if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units);
ptr2 += units;
repl = false;
}
ptr1 += units;
cur_pos += (size_t)units;
}
*ptr2 = '\0';
}
} // namespace sphaira::title

View File

@@ -30,112 +30,6 @@
namespace sphaira::ui::menu::game {
namespace {
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
constexpr int THREAD_CORE = 1;
// taken from nxtc
constexpr u8 g_nacpLangTable[SetLanguage_Total] = {
[SetLanguage_JA] = 2,
[SetLanguage_ENUS] = 0,
[SetLanguage_FR] = 3,
[SetLanguage_DE] = 4,
[SetLanguage_IT] = 7,
[SetLanguage_ES] = 6,
[SetLanguage_ZHCN] = 14,
[SetLanguage_KO] = 12,
[SetLanguage_NL] = 8,
[SetLanguage_PT] = 10,
[SetLanguage_RU] = 11,
[SetLanguage_ZHTW] = 13,
[SetLanguage_ENGB] = 1,
[SetLanguage_FRCA] = 9,
[SetLanguage_ES419] = 5,
[SetLanguage_ZHHANS] = 14,
[SetLanguage_ZHHANT] = 13,
[SetLanguage_PTBR] = 15
};
auto GetNacpLangEntryIndex() -> u8 {
SetLanguage lang{SetLanguage_ENUS};
nxtcGetCacheLanguage(&lang);
return g_nacpLangTable[lang];
}
constexpr u32 ContentMetaTypeToContentFlag(u8 meta_type) {
if (meta_type & 0x80) {
return 1 << (meta_type - 0x80);
}
return 0;
}
enum ContentFlag {
ContentFlag_Application = ContentMetaTypeToContentFlag(NcmContentMetaType_Application),
ContentFlag_Patch = ContentMetaTypeToContentFlag(NcmContentMetaType_Patch),
ContentFlag_AddOnContent = ContentMetaTypeToContentFlag(NcmContentMetaType_AddOnContent),
ContentFlag_DataPatch = ContentMetaTypeToContentFlag(NcmContentMetaType_DataPatch),
ContentFlag_All = ContentFlag_Application | ContentFlag_Patch | ContentFlag_AddOnContent | ContentFlag_DataPatch,
};
struct NcmEntry {
const NcmStorageId storage_id;
NcmContentStorage cs{};
NcmContentMetaDatabase db{};
void Open() {
if (R_FAILED(ncmOpenContentMetaDatabase(std::addressof(db), storage_id))) {
log_write("\tncmOpenContentMetaDatabase() failed. storage_id: %u\n", storage_id);
} else {
log_write("\tncmOpenContentMetaDatabase() success. storage_id: %u\n", storage_id);
}
if (R_FAILED(ncmOpenContentStorage(std::addressof(cs), storage_id))) {
log_write("\tncmOpenContentStorage() failed. storage_id: %u\n", storage_id);
} else {
log_write("\tncmOpenContentStorage() success. storage_id: %u\n", storage_id);
}
}
void Close() {
ncmContentMetaDatabaseClose(std::addressof(db));
ncmContentStorageClose(std::addressof(cs));
db = {};
cs = {};
}
};
constinit NcmEntry ncm_entries[] = {
// on memory, will become invalid on the gamecard being inserted / removed.
{ NcmStorageId_GameCard },
// normal (save), will remain valid.
{ NcmStorageId_BuiltInUser },
{ NcmStorageId_SdCard },
};
auto& GetNcmEntry(u8 storage_id) {
auto it = std::ranges::find_if(ncm_entries, [storage_id](auto& e){
return storage_id == e.storage_id;
});
if (it == std::end(ncm_entries)) {
log_write("unable to find valid ncm entry: %u\n", storage_id);
return ncm_entries[0];
}
return *it;
}
auto& GetNcmCs(u8 storage_id) {
return GetNcmEntry(storage_id).cs;
}
auto& GetNcmDb(u8 storage_id) {
return GetNcmEntry(storage_id).db;
}
using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
struct ContentInfoEntry {
NsApplicationContentMetaStatus status{};
std::vector<NcmContentInfo> content_infos{};
@@ -287,36 +181,8 @@ Result Notify(Result rc, const std::string& error_message) {
return rc;
}
Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags = ContentFlag_All) {
for (s32 i = 0; ; i++) {
s32 count;
NsApplicationContentMetaStatus status;
R_TRY(nsListApplicationContentMetaStatus(id, i, &status, 1, &count));
if (!count) {
break;
}
if (flags & ContentMetaTypeToContentFlag(status.meta_type)) {
out.emplace_back(status);
}
}
R_SUCCEED();
}
Result GetMetaEntries(const Entry& e, MetaEntries& out, u32 flags = ContentFlag_All) {
return GetMetaEntries(e.app_id, out, flags);
}
// also sets the status to error.
void FakeNacpEntry(ThreadResultData& e) {
e.status = NacpLoadStatus::Error;
// fake the nacp entry
std::strcpy(e.lang.name, "Corrupted");
std::strcpy(e.lang.author, "Corrupted");
e.control.reset();
Result GetMetaEntries(const Entry& e, title::MetaEntries& out, u32 flags = title::ContentFlag_All) {
return title::GetMetaEntries(e.app_id, out, flags);
}
bool LoadControlImage(Entry& e) {
@@ -335,92 +201,7 @@ bool LoadControlImage(Entry& e) {
return false;
}
Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path) {
const auto& ee = status;
if (ee.storageID != NcmStorageId_SdCard && ee.storageID != NcmStorageId_BuiltInUser && ee.storageID != NcmStorageId_GameCard) {
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));
R_TRY(ncmContentStorageGetProgramId(&cs, out_program_id, &content_id, FsContentAttributes_All));
R_TRY(ncmContentStorageGetPath(&cs, out_path->s, sizeof(*out_path), &content_id));
R_SUCCEED();
}
Result LoadControlManual(u64 id, ThreadResultData& data) {
TimeStamp ts;
MetaEntries entries;
R_TRY(GetMetaEntries(id, entries));
R_UNLESS(!entries.empty(), Result_GameEmptyMetaEntries);
u64 program_id;
fs::FsPath path;
R_TRY(GetControlPathFromStatus(entries.back(), &program_id, &path));
std::vector<u8> icon;
R_TRY(nca::ParseControl(path, program_id, &data.control->nacp.lang[GetNacpLangEntryIndex()], sizeof(NacpLanguageEntry), &icon));
std::memcpy(data.control->icon, icon.data(), icon.size());
data.jpeg_size = 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 = true;
if (hosversionBefore(20,0,0)) {
TimeStamp ts;
u64 actual_size;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, data.control.get(), sizeof(NsApplicationControlData), &actual_size))) {
manual_load = false;
data.jpeg_size = actual_size - sizeof(NacpStruct);
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;
u64 actual_size;
if (R_SUCCEEDED(rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, id, data.control.get(), sizeof(NsApplicationControlData), &actual_size))) {
data.jpeg_size = actual_size - sizeof(NacpStruct);
log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
}
}
if (R_SUCCEEDED(rc)) {
data.lang = data.control->nacp.lang[GetNacpLangEntryIndex()];
data.status = NacpLoadStatus::Loaded;
}
if (R_FAILED(rc)) {
FakeNacpEntry(data);
}
return data;
}
void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) {
void LoadResultIntoEntry(Entry& e, const title::ThreadResultData& result) {
e.status = result.status;
e.control = result.control;
e.jpeg_size= result.jpeg_size;
@@ -429,56 +210,16 @@ void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) {
}
void LoadControlEntry(Entry& e, bool force_image_load = false) {
if (e.status == NacpLoadStatus::None) {
const auto result = LoadControlEntry(e.app_id);
if (e.status == title::NacpLoadStatus::None) {
const auto result = title::LoadControlEntry(e.app_id);
LoadResultIntoEntry(e, result);
}
if (force_image_load && e.status == NacpLoadStatus::Loaded) {
if (force_image_load && e.status == title::NacpLoadStatus::Loaded) {
LoadControlImage(e);
}
}
// taken from nxdumptool.
void utilsReplaceIllegalCharacters(char *str, bool ascii_only)
{
static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|";
size_t str_size = 0, cur_pos = 0;
if (!str || !(str_size = strlen(str))) return;
u8 *ptr1 = (u8*)str, *ptr2 = ptr1;
ssize_t units = 0;
u32 code = 0;
bool repl = false;
while(cur_pos < str_size)
{
units = decode_utf8(&code, ptr1);
if (units < 0) break;
if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \
(units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars))))
{
if (!repl)
{
*ptr2++ = '_';
repl = true;
}
} else {
if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units);
ptr2 += units;
repl = false;
}
ptr1 += units;
cur_pos += (size_t)units;
}
*ptr2 = '\0';
}
auto isRightsIdValid(FsRightsId id) -> bool {
FsRightsId empty_id{};
return 0 != std::memcmp(std::addressof(id), std::addressof(empty_id), sizeof(id));
@@ -498,13 +239,13 @@ HashStr hexIdToStr(auto id) {
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath {
fs::FsPath name_buf = e.GetName();
utilsReplaceIllegalCharacters(name_buf, true);
title::utilsReplaceIllegalCharacters(name_buf, true);
char version[sizeof(NacpStruct::display_version) + 1]{};
if (status.meta_type == NcmContentMetaType_Patch) {
u64 program_id;
fs::FsPath path;
if (R_SUCCEEDED(GetControlPathFromStatus(status, &program_id, &path))) {
if (R_SUCCEEDED(title::GetControlPathFromStatus(status, &program_id, &path))) {
char display_version[0x10];
if (R_SUCCEEDED(nca::ParseControl(path, program_id, display_version, sizeof(display_version), nullptr, offsetof(NacpStruct, display_version)))) {
std::snprintf(version, sizeof(version), "%s ", display_version);
@@ -523,8 +264,8 @@ auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status)
}
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out) {
auto& cs = GetNcmCs(status.storageID);
auto& db = GetNcmDb(status.storageID);
auto& cs = title::GetNcmCs(status.storageID);
auto& db = title::GetNcmDb(status.storageID);
const auto app_id = ncm::GetAppId(status.meta_type, status.application_id);
auto id_min = status.application_id;
@@ -633,7 +374,7 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::K
}
out.nsp_data = yati::container::Nsp::Build(out.collections, out.nsp_size);
out.cs = GetNcmCs(info.status.storageID);
out.cs = title::GetNcmCs(info.status.storageID);
R_SUCCEED();
}
@@ -641,7 +382,7 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::K
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out) {
LoadControlEntry(e);
MetaEntries meta_entries;
title::MetaEntries meta_entries;
R_TRY(GetMetaEntries(e, meta_entries, flags));
keys::Keys keys;
@@ -670,114 +411,8 @@ void LaunchEntry(const Entry& e) {
Notify(rc, "Failed to launch application");
}
void ThreadFunc(void* user) {
auto data = static_cast<ThreadData*>(user);
if (data->IsTitleCacheEnabled() && !nxtcInitialize()) {
log_write("[NXTC] failed to init cache\n");
}
ON_SCOPE_EXIT(nxtcExit());
while (data->IsRunning()) {
data->Run();
}
}
} // namespace
ThreadData::ThreadData(bool title_cache) : m_title_cache{title_cache} {
ueventCreate(&m_uevent, true);
mutexInit(&m_mutex_id);
mutexInit(&m_mutex_result);
m_running = true;
}
void ThreadData::Run() {
const auto waiter = waiterForUEvent(&m_uevent);
while (IsRunning()) {
const auto rc = waitSingle(waiter, 3e+9);
// if we timed out, flush the cache and poll again.
if (R_FAILED(rc)) {
nxtcFlushCacheFile();
continue;
}
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;
}
ThreadResultData result{ids[i]};
TimeStamp ts;
if (auto data = nxtcGetApplicationMetadataEntryById(ids[i])) {
log_write("[NXTC] loaded from cache time taken: %.2fs %zums %zuns\n", ts.GetSecondsD(), ts.GetMs(), ts.GetNs());
ON_SCOPE_EXIT(nxtcFreeApplicationMetadata(&data));
result.control = std::make_unique<NsApplicationControlData>();
result.status = NacpLoadStatus::Loaded;
std::strcpy(result.lang.name, data->name);
std::strcpy(result.lang.author, data->publisher);
std::memcpy(result.control->icon, data->icon_data, data->icon_size);
result.jpeg_size = data->icon_size;
} else {
// sleep after every other entry loaded.
svcSleepThread(2e+6); // 2ms
result = LoadControlEntry(ids[i]);
if (result.status == NacpLoadStatus::Loaded) {
nxtcAddEntry(ids[i], &result.control->nacp, result.jpeg_size, result.control->icon, true);
}
}
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::ranges::find(m_ids, 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(u32 flags) : grid::Menu{"Games"_i18n, flags} {
this->SetActions(
std::make_pair(Button::L3, Action{[this](){
@@ -880,7 +515,7 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
}));
options->Add(std::make_shared<SidebarEntryCallback>("List meta records"_i18n, [this](){
MetaEntries meta_entries;
title::MetaEntries meta_entries;
const auto rc = GetMetaEntries(m_entries[m_index], meta_entries);
if (R_FAILED(rc)) {
App::Push(std::make_shared<ui::ErrorBox>(rc,
@@ -917,19 +552,19 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("Dump All"_i18n, [this](){
DumpGames(ContentFlag_All);
DumpGames(title::ContentFlag_All);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump Application"_i18n, [this](){
DumpGames(ContentFlag_Application);
DumpGames(title::ContentFlag_Application);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump Patch"_i18n, [this](){
DumpGames(ContentFlag_Patch);
DumpGames(title::ContentFlag_Patch);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump AddOnContent"_i18n, [this](){
DumpGames(ContentFlag_AddOnContent);
DumpGames(title::ContentFlag_AddOnContent);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump DataPatch"_i18n, [this](){
DumpGames(ContentFlag_DataPatch);
DumpGames(title::ContentFlag_DataPatch);
}, true));
}, true));
@@ -988,30 +623,15 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
nsInitialize();
es::Initialize();
for (auto& e : ncm_entries) {
e.Open();
}
m_thread_data = std::make_unique<ThreadData>(m_title_cache.Get());
threadCreate(&m_thread, ThreadFunc, m_thread_data.get(), nullptr, 1024*32, THREAD_PRIO, THREAD_CORE);
svcSetThreadCoreMask(m_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE));
threadStart(&m_thread);
title::Init();
}
Menu::~Menu() {
m_thread_data->Close();
for (auto& e : ncm_entries) {
e.Close();
}
title::Exit();
FreeEntries();
nsExit();
es::Exit();
threadWaitForExit(&m_thread);
threadClose(&m_thread);
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
@@ -1043,26 +663,17 @@ 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::ranges::find_if(m_entries, [&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) {
m_thread_data->Push(e.app_id);
e.status = NacpLoadStatus::Progress;
if (e.status == title::NacpLoadStatus::None) {
title::Push(e.app_id);
e.status = title::NacpLoadStatus::Progress;
} else if (e.status == title::NacpLoadStatus::Progress) {
if (const auto data = title::Get(e.app_id)) {
LoadResultIntoEntry(e, *data);
}
}
// lazy load image

View File

@@ -14,6 +14,7 @@
#include "download.hpp"
#include "dumper.hpp"
#include "image.hpp"
#include "title_info.hpp"
#include <cstring>
#include <algorithm>
@@ -62,46 +63,6 @@ auto GetXciSizeFromRomSize(u8 rom_size) -> s64 {
return 0;
}
// taken from nxdumptool.
void utilsReplaceIllegalCharacters(char *str, bool ascii_only)
{
static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|";
size_t str_size = 0, cur_pos = 0;
if (!str || !(str_size = strlen(str))) return;
u8 *ptr1 = (u8*)str, *ptr2 = ptr1;
ssize_t units = 0;
u32 code = 0;
bool repl = false;
while(cur_pos < str_size)
{
units = decode_utf8(&code, ptr1);
if (units < 0) break;
if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \
(units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars))))
{
if (!repl)
{
*ptr2++ = '_';
repl = true;
}
} else {
if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units);
ptr2 += units;
repl = false;
}
ptr1 += units;
cur_pos += (size_t)units;
}
*ptr2 = '\0';
}
struct DebugEventInfo {
u32 event_type;
u32 flags;
@@ -132,7 +93,7 @@ auto GetDumpTypeStr(u8 type) -> const char* {
auto BuildXciName(const ApplicationEntry& e) -> fs::FsPath {
fs::FsPath name_buf = e.lang_entry.name;
utilsReplaceIllegalCharacters(name_buf, true);
title::utilsReplaceIllegalCharacters(name_buf, true);
fs::FsPath path;
std::snprintf(path, sizeof(path), "%s [%016lX][v%u]", name_buf.s, e.app_id, e.version);
@@ -475,18 +436,18 @@ Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
const Vec2 pad{0, 125 - v.h};
m_list = std::make_unique<List>(1, 3, m_pos, v, pad);
nsInitialize();
fsOpenDeviceOperator(std::addressof(m_dev_op));
fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier));
fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true);
title::Init();
}
Menu::~Menu() {
title::Exit();
GcUnmount();
eventClose(std::addressof(m_event));
fsEventNotifierClose(std::addressof(m_event_notifier));
fsDeviceOperatorClose(std::addressof(m_dev_op));
nsExit();
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
@@ -683,13 +644,6 @@ Result Menu::GcMount() {
// load all control data, icons are loaded when displayed.
for (auto& e : m_entries) {
R_TRY(LoadControlData(e));
NacpLanguageEntry* lang_entry{};
R_TRY(nacpGetLanguageEntry(&e.control->nacp, &lang_entry));
if (lang_entry) {
e.lang_entry = *lang_entry;
}
}
if (m_entries.size() > 1) {
@@ -909,47 +863,13 @@ void Menu::FreeImage() {
}
Result Menu::LoadControlData(ApplicationEntry& e) {
const auto id = e.app_id;
e.control = std::make_unique<NsApplicationControlData>();
const auto data = title::LoadControlEntry(e.app_id);
R_UNLESS(data.status == title::NacpLoadStatus::Loaded, 0x1);
if (hosversionBefore(20,0,0)) {
TimeStamp ts;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, e.control.get(), sizeof(NsApplicationControlData), &e.control_size))) {
log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
R_SUCCEED();
}
}
// nsGetApplicationControlData() will fail if it's the first time
// mounting a gamecard if the image is not already cached.
// waiting 1-2s after mount, then calling seems to work.
// however, we can just manually parse the nca to get the data we need,
// which always works and *is* faster too ;)
for (const auto& app : e.application) {
for (const auto& collection : app) {
if (collection.type == NcmContentType_Control) {
const auto path = BuildGcPath(collection.name.c_str(), &m_handle);
u64 program_id = id | collection.id_offset;
if (hosversionAtLeast(17, 0, 0)) {
fsGetProgramId(&program_id, path, FsContentAttributes_All);
}
TimeStamp ts;
std::vector<u8> icon;
if (R_SUCCEEDED(nca::ParseControl(path, program_id, &e.control->nacp, sizeof(e.control->nacp), &icon))) {
std::memcpy(e.control->icon, icon.data(), icon.size());
e.control_size = sizeof(e.control->nacp) + icon.size();
log_write("\t\tnca::ParseControl(): %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
R_SUCCEED();
} else {
log_write("\tFAILED to parse control nca %s\n", path.s);
}
}
}
}
return 0x1;
e.control = data.control;
e.jpeg_size = data.jpeg_size;
e.lang_entry = data.lang;
R_SUCCEED();
}
void Menu::OnChangeIndex(s64 new_index) {
@@ -963,7 +883,7 @@ void Menu::OnChangeIndex(s64 new_index) {
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
const auto& e = m_entries[m_entry_index];
const auto jpeg_size = e.control_size - sizeof(NacpStruct);
const auto jpeg_size = e.jpeg_size;
TimeStamp ts;
const auto image = ImageLoadFromMemory({e.control->icon, jpeg_size}, ImageFlag_JPEG);

View File

@@ -34,9 +34,6 @@
namespace sphaira::ui::menu::save {
namespace {
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
constexpr int THREAD_CORE = 1;
constexpr u32 NX_SAVE_META_MAGIC = 0x4A4B5356; // JKSV
constexpr u32 NX_SAVE_META_VERSION = 1;
constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin";
@@ -59,34 +56,6 @@ struct NXSaveMeta {
};
static_assert(sizeof(NXSaveMeta) == 128);
// taken from nxtc
constexpr u8 g_nacpLangTable[SetLanguage_Total] = {
[SetLanguage_JA] = 2,
[SetLanguage_ENUS] = 0,
[SetLanguage_FR] = 3,
[SetLanguage_DE] = 4,
[SetLanguage_IT] = 7,
[SetLanguage_ES] = 6,
[SetLanguage_ZHCN] = 14,
[SetLanguage_KO] = 12,
[SetLanguage_NL] = 8,
[SetLanguage_PT] = 10,
[SetLanguage_RU] = 11,
[SetLanguage_ZHTW] = 13,
[SetLanguage_ENGB] = 1,
[SetLanguage_FRCA] = 9,
[SetLanguage_ES419] = 5,
[SetLanguage_ZHHANS] = 14,
[SetLanguage_ZHHANT] = 13,
[SetLanguage_PTBR] = 15
};
auto GetNacpLangEntryIndex() -> u8 {
SetLanguage lang{SetLanguage_ENUS};
nxtcGetCacheLanguage(&lang);
return g_nacpLangTable[lang];
}
void GetFsSaveAttr(const AccountProfileBase& acc, u8 data_type, FsSaveDataSpaceId& space_id, FsSaveDataFilter& filter) {
std::memset(&filter, 0, sizeof(filter));
@@ -117,108 +86,6 @@ void GetFsSaveAttr(const AccountProfileBase& acc, u8 data_type, FsSaveDataSpaceI
}
}
constexpr u32 ContentMetaTypeToContentFlag(u8 meta_type) {
if (meta_type & 0x80) {
return 1 << (meta_type - 0x80);
}
return 0;
}
enum ContentFlag {
ContentFlag_Application = ContentMetaTypeToContentFlag(NcmContentMetaType_Application),
ContentFlag_Patch = ContentMetaTypeToContentFlag(NcmContentMetaType_Patch),
ContentFlag_AddOnContent = ContentMetaTypeToContentFlag(NcmContentMetaType_AddOnContent),
ContentFlag_DataPatch = ContentMetaTypeToContentFlag(NcmContentMetaType_DataPatch),
ContentFlag_All = ContentFlag_Application | ContentFlag_Patch | ContentFlag_AddOnContent | ContentFlag_DataPatch,
};
struct NcmEntry {
const NcmStorageId storage_id;
NcmContentStorage cs{};
NcmContentMetaDatabase db{};
void Open() {
if (R_FAILED(ncmOpenContentMetaDatabase(std::addressof(db), storage_id))) {
log_write("\tncmOpenContentMetaDatabase() failed. storage_id: %u\n", storage_id);
} else {
log_write("\tncmOpenContentMetaDatabase() success. storage_id: %u\n", storage_id);
}
if (R_FAILED(ncmOpenContentStorage(std::addressof(cs), storage_id))) {
log_write("\tncmOpenContentStorage() failed. storage_id: %u\n", storage_id);
} else {
log_write("\tncmOpenContentStorage() success. storage_id: %u\n", storage_id);
}
}
void Close() {
ncmContentMetaDatabaseClose(std::addressof(db));
ncmContentStorageClose(std::addressof(cs));
db = {};
cs = {};
}
};
constinit NcmEntry ncm_entries[] = {
// on memory, will become invalid on the gamecard being inserted / removed.
{ NcmStorageId_GameCard },
// normal (save), will remain valid.
{ NcmStorageId_BuiltInUser },
{ NcmStorageId_SdCard },
};
auto& GetNcmEntry(u8 storage_id) {
auto it = std::ranges::find_if(ncm_entries, [storage_id](auto& e){
return storage_id == e.storage_id;
});
if (it == std::end(ncm_entries)) {
log_write("unable to find valid ncm entry: %u\n", storage_id);
return ncm_entries[0];
}
return *it;
}
auto& GetNcmCs(u8 storage_id) {
return GetNcmEntry(storage_id).cs;
}
auto& GetNcmDb(u8 storage_id) {
return GetNcmEntry(storage_id).db;
}
using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags = ContentFlag_All) {
for (s32 i = 0; ; i++) {
s32 count;
NsApplicationContentMetaStatus status;
R_TRY(nsListApplicationContentMetaStatus(id, i, &status, 1, &count));
if (!count) {
break;
}
if (flags & ContentMetaTypeToContentFlag(status.meta_type)) {
out.emplace_back(status);
}
}
R_SUCCEED();
}
// also sets the status to error.
void FakeNacpEntry(ThreadResultData& e) {
e.status = NacpLoadStatus::Error;
// fake the nacp entry
std::strcpy(e.lang.name, "Corrupted");
std::strcpy(e.lang.author, "Corrupted");
e.control.reset();
}
auto GetSaveFolder(u8 data_type) -> fs::FsPath {
switch (data_type) {
case FsSaveDataType_System: return "Save System";
@@ -344,7 +211,7 @@ auto GetSystemSaveName(u64 system_save_data_id) -> const char* {
}
void FakeNacpEntryForSystem(Entry& e) {
e.status = NacpLoadStatus::Loaded;
e.status = title::NacpLoadStatus::Loaded;
// fake the nacp entry
std::snprintf(e.lang.name, sizeof(e.lang.name), "%s | %016lX", GetSystemSaveName(e.system_save_data_id), e.system_save_data_id);
@@ -368,92 +235,7 @@ bool LoadControlImage(Entry& e) {
return false;
}
Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path) {
const auto& ee = status;
if (ee.storageID != NcmStorageId_SdCard && ee.storageID != NcmStorageId_BuiltInUser && ee.storageID != NcmStorageId_GameCard) {
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));
R_TRY(ncmContentStorageGetProgramId(&cs, out_program_id, &content_id, FsContentAttributes_All));
R_TRY(ncmContentStorageGetPath(&cs, out_path->s, sizeof(*out_path), &content_id));
R_SUCCEED();
}
Result LoadControlManual(u64 id, ThreadResultData& data) {
TimeStamp ts;
MetaEntries entries;
R_TRY(GetMetaEntries(id, entries));
R_UNLESS(!entries.empty(), Result_GameEmptyMetaEntries);
u64 program_id;
fs::FsPath path;
R_TRY(GetControlPathFromStatus(entries.back(), &program_id, &path));
std::vector<u8> icon;
R_TRY(nca::ParseControl(path, program_id, &data.control->nacp.lang[GetNacpLangEntryIndex()], sizeof(NacpLanguageEntry), &icon));
std::memcpy(data.control->icon, icon.data(), icon.size());
data.jpeg_size = 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 = true;
if (hosversionBefore(20,0,0)) {
TimeStamp ts;
u64 actual_size;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, data.control.get(), sizeof(NsApplicationControlData), &actual_size))) {
manual_load = false;
data.jpeg_size = actual_size - sizeof(NacpStruct);
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;
u64 actual_size;
if (R_SUCCEEDED(rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, id, data.control.get(), sizeof(NsApplicationControlData), &actual_size))) {
data.jpeg_size = actual_size - sizeof(NacpStruct);
log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
}
}
if (R_SUCCEEDED(rc)) {
data.lang = data.control->nacp.lang[GetNacpLangEntryIndex()];
data.status = NacpLoadStatus::Loaded;
}
if (R_FAILED(rc)) {
FakeNacpEntry(data);
}
return data;
}
void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) {
void LoadResultIntoEntry(Entry& e, const title::ThreadResultData& result) {
e.status = result.status;
e.control = result.control;
e.jpeg_size= result.jpeg_size;
@@ -462,60 +244,20 @@ void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) {
}
void LoadControlEntry(Entry& e, bool force_image_load = false) {
if (e.status == NacpLoadStatus::None) {
if (e.status == title::NacpLoadStatus::None) {
if (e.save_data_type == FsSaveDataType_System || e.save_data_type == FsSaveDataType_SystemBcat) {
FakeNacpEntryForSystem(e);
} else {
const auto result = LoadControlEntry(e.application_id);
const auto result = title::LoadControlEntry(e.application_id);
LoadResultIntoEntry(e, result);
}
}
if (force_image_load && e.status == NacpLoadStatus::Loaded) {
if (force_image_load && e.status == title::NacpLoadStatus::Loaded) {
LoadControlImage(e);
}
}
// taken from nxdumptool.
void utilsReplaceIllegalCharacters(char *str, bool ascii_only)
{
static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|";
size_t str_size = 0, cur_pos = 0;
if (!str || !(str_size = strlen(str))) return;
u8 *ptr1 = (u8*)str, *ptr2 = ptr1;
ssize_t units = 0;
u32 code = 0;
bool repl = false;
while(cur_pos < str_size)
{
units = decode_utf8(&code, ptr1);
if (units < 0) break;
if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \
(units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars))))
{
if (!repl)
{
*ptr2++ = '_';
repl = true;
}
} else {
if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units);
ptr2 += units;
repl = false;
}
ptr1 += units;
cur_pos += (size_t)units;
}
*ptr2 = '\0';
}
struct HashStr {
char str[0x21];
};
@@ -530,7 +272,7 @@ HashStr hexIdToStr(auto id) {
auto BuildSaveName(const Entry& e) -> fs::FsPath {
fs::FsPath name_buf = e.GetName();
utilsReplaceIllegalCharacters(name_buf, true);
title::utilsReplaceIllegalCharacters(name_buf, true);
return name_buf;
}
@@ -550,122 +292,12 @@ void FreeEntry(NVGcontext* vg, Entry& e) {
e.image = 0;
}
void ThreadFunc(void* user) {
auto data = static_cast<ThreadData*>(user);
if (!nxtcInitialize()) {
log_write("[NXTC] failed to init cache\n");
}
ON_SCOPE_EXIT(nxtcExit());
while (data->IsRunning()) {
data->Run();
}
}
} // namespace
void SignalChange() {
ueventSignal(&g_change_uevent);
}
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() {
const auto waiter = waiterForUEvent(&m_uevent);
while (IsRunning()) {
const auto rc = waitSingle(waiter, 3e+9);
// if we timed out, flush the cache and poll again.
if (R_FAILED(rc)) {
nxtcFlushCacheFile();
continue;
}
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;
}
ThreadResultData result{ids[i]};
TimeStamp ts;
if (auto data = nxtcGetApplicationMetadataEntryById(ids[i])) {
log_write("[NXTC] loaded from cache time taken: %.2fs %zums %zuns\n", ts.GetSecondsD(), ts.GetMs(), ts.GetNs());
ON_SCOPE_EXIT(nxtcFreeApplicationMetadata(&data));
result.control = std::make_unique<NsApplicationControlData>();
result.status = NacpLoadStatus::Loaded;
std::strcpy(result.lang.name, data->name);
std::strcpy(result.lang.author, data->publisher);
std::memcpy(result.control->icon, data->icon_data, data->icon_size);
result.jpeg_size = data->icon_size;
} else {
// sleep after every other entry loaded.
svcSleepThread(2e+6); // 2ms
result = LoadControlEntry(ids[i]);
if (result.status == NacpLoadStatus::Loaded) {
nxtcAddEntry(ids[i], &result.control->nacp, result.jpeg_size, result.control->icon, true);
}
}
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::ranges::find(m_ids, 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.application_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(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
this->SetActions(
std::make_pair(Button::L3, Action{[this](){
@@ -820,28 +452,15 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
log_write("[SAVE] account uid is not found: 0x%016lX%016lX\n", uid.uid[0], uid.uid[1]);
}
for (auto& e : ncm_entries) {
e.Open();
}
threadCreate(&m_thread, ThreadFunc, &m_thread_data, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE);
svcSetThreadCoreMask(m_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE));
threadStart(&m_thread);
title::Init();
ueventCreate(&g_change_uevent, true);
}
Menu::~Menu() {
m_thread_data.Close();
for (auto& e : ncm_entries) {
e.Close();
}
title::Exit();
FreeEntries();
nsExit();
threadWaitForExit(&m_thread);
threadClose(&m_thread);
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
@@ -877,30 +496,21 @@ 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) {
for (auto& e : m_entries) {
if (e.application_id == d.id) {
// don't break out of loop as multiple entries may use
// the same tid, such as cached saves.
LoadResultIntoEntry(e, 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) {
if (e.status == title::NacpLoadStatus::None) {
if (m_data_type != FsSaveDataType_System && m_data_type != FsSaveDataType_SystemBcat) {
m_thread_data.Push(e.application_id);
e.status = NacpLoadStatus::Progress;
title::Push(e.application_id);
e.status = title::NacpLoadStatus::Progress;
} else {
FakeNacpEntryForSystem(e);
}
} else if (e.status == title::NacpLoadStatus::Progress) {
if (const auto data = title::Get(e.application_id)) {
LoadResultIntoEntry(e, *data);
}
}
// lazy load image
@@ -1185,7 +795,7 @@ auto Menu::BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath {
std::snprintf(name_buf, sizeof(name_buf), "%s", acc.nickname);
}
utilsReplaceIllegalCharacters(name_buf, true);
title::utilsReplaceIllegalCharacters(name_buf, true);
std::snprintf(path, sizeof(path), "%s/%s - %s.zip", base.s, name_buf.s, time);
} else {
std::snprintf(path, sizeof(path), "%s/%s.zip", base.s, time);