From 4be1d48215f1448e253f76ed0629890eac094342 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Mon, 2 Jun 2025 22:18:38 +0100 Subject: [PATCH] use oss-nvjpg for loading jpeg images (homebrew, games and themezer). slightly faster loading on avg compared to stbi. --- README.md | 31 +++++++------ sphaira/CMakeLists.txt | 15 +++++++ sphaira/include/app.hpp | 3 ++ sphaira/include/image.hpp | 10 ++++- sphaira/source/app.cpp | 10 +++++ sphaira/source/image.cpp | 65 +++++++++++++++++++++++---- sphaira/source/ui/menus/game_menu.cpp | 13 ++++-- sphaira/source/ui/menus/gc_menu.cpp | 9 +++- sphaira/source/ui/menus/homebrew.cpp | 13 +++++- sphaira/source/ui/menus/themezer.cpp | 20 ++++----- 10 files changed, 146 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 1adeb09..252c267 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A homebrew menu for the Nintendo Switch. [See the GBATemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/). -[We have now have a Discord server!](https://discord.gg/8vZBsrprEc). Please use the issues tab to report bugs, as it is much easier for me to track. +[We have now have a Discord server!](https://discord.gg/8vZBsrprEc) Please use the issues tab to report bugs, as it is much easier for me to track. ## Showcase @@ -86,16 +86,21 @@ The output will be found in `build/MinSizeRel/sphaira.nro` ## Credits -- borealis -- stb -- yyjson -- nx-hbmenu -- nx-hbloader -- deko3d-nanovg -- libpulsar -- minIni -- GBATemp -- hb-appstore -- haze -- nxdumptool (for gamecard bin dumping and rsa verify code) +- [borealis](https://github.com/natinusala/borealis) +- [stb](https://github.com/nothings/stb) +- [yyjson](https://github.com/ibireme/yyjson) +- [nx-hbmenu](https://github.com/switchbrew/nx-hbmenu) +- [nx-hbloader](https://github.com/switchbrew/nx-hbloader) +- [deko3d-nanovg](https://github.com/Adubbz/nanovg-deko3d) +- [libpulsar](https://github.com/p-sam/switch-libpulsar) +- [minIni](https://github.com/compuphase/minIni) +- [GBATemp](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/) +- [hb-appstore](https://github.com/fortheusers/hb-appstore) +- [haze](https://github.com/Atmosphere-NX/Atmosphere/tree/master/troposphere/haze) +- [nxdumptool](https://github.com/DarkMatterCore/nxdumptool) (for gamecard bin dumping and rsa verify code) +- [libusbhsfs](https://github.com/DarkMatterCore/libusbhsfs) +- [libnxtc](https://github.com/DarkMatterCore/libnxtc) +- [oss-nvjpg](https://github.com/averne/oss-nvjpg) +- [nsz](https://github.com/nicoboss/nsz) +- [themezer](https://themezer.net/) - Everyone who has contributed to this project! diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index dcf635d..12d9a47 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -202,6 +202,11 @@ FetchContent_Declare(libnxtc GIT_TAG v0.0.2 ) +FetchContent_Declare(nvjpg + GIT_REPOSITORY https://github.com/averne/oss-nvjpg.git + GIT_TAG fdcaba8 +) + set(USE_NEW_ZSTD ON) set(ZSTD_BUILD_STATIC ON) @@ -255,6 +260,7 @@ FetchContent_MakeAvailable( zstd libusbhsfs libnxtc + nvjpg ) set(FTPSRV_LIB_BUILD TRUE) @@ -304,6 +310,14 @@ add_library(libnxtc ) target_include_directories(libnxtc PUBLIC ${libnxtc_SOURCE_DIR}/include) +add_library(nvjpg + ${nvjpg_SOURCE_DIR}/lib/decoder.cpp + ${nvjpg_SOURCE_DIR}/lib/image.cpp + ${nvjpg_SOURCE_DIR}/lib/surface.cpp +) +target_include_directories(nvjpg PUBLIC ${nvjpg_SOURCE_DIR}/include) +set_target_properties(nvjpg PROPERTIES CXX_STANDARD 26) + find_package(ZLIB REQUIRED) find_library(minizip_lib minizip REQUIRED) find_path(minizip_inc minizip REQUIRED) @@ -334,6 +348,7 @@ target_link_libraries(sphaira PRIVATE yyjson # libusbhsfs libnxtc + nvjpg ${minizip_lib} ZLIB::ZLIB diff --git a/sphaira/include/app.hpp b/sphaira/include/app.hpp index 5b14f14..daae443 100644 --- a/sphaira/include/app.hpp +++ b/sphaira/include/app.hpp @@ -10,6 +10,7 @@ #include "fs.hpp" #include "log.hpp" +#include #include #include #include @@ -272,6 +273,8 @@ public: PLSR_PlayerSoundId m_sound_ids[SoundEffect_MAX]{}; + nj::Decoder m_decoder; + private: // from nanovg decko3d example by adubbz static constexpr unsigned NumFramebuffers = 2; static constexpr unsigned StaticCmdSize = 0x1000; diff --git a/sphaira/include/image.hpp b/sphaira/include/image.hpp index f589231..649656c 100644 --- a/sphaira/include/image.hpp +++ b/sphaira/include/image.hpp @@ -12,8 +12,14 @@ struct ImageResult { int w, h; }; -auto ImageLoadFromMemory(std::span data) -> ImageResult; -auto ImageLoadFromFile(const fs::FsPath& file) -> ImageResult; +enum ImageFlag { + ImageFlag_None = 0, + // set this if the image is a jpeg, will use oss-nvjpg to load. + ImageFlag_JPEG = 1 << 0, +}; + +auto ImageLoadFromMemory(std::span data, u32 flags = ImageFlag_None) -> ImageResult; +auto ImageLoadFromFile(const fs::FsPath& file, u32 flags = ImageFlag_None) -> ImageResult; auto ImageResize(std::span data, int inx, int iny, int outx, int outy) -> ImageResult; auto ImageConvertToJpg(std::span data, int x, int y) -> ImageResult; diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 4da6d94..fc89fa9 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -1400,6 +1400,10 @@ App::App(const char* argv0) { curl::Init(); + // this has to be init before deko3d. + nj::initialize(); + m_decoder.initialize(); + // get current size of the framebuffer const auto fb = GetFrameBufferSize(); s_width = fb.size.x; @@ -1849,6 +1853,7 @@ App::~App() { ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal)); log_write("starting to exit\n"); + TimeStamp ts; i18n::exit(); curl::Exit(); @@ -1878,6 +1883,9 @@ App::~App() { nvgDeleteDk(this->vg); this->renderer.reset(); + m_decoder.finalize(); + nj::finalize(); + // backup hbmenu if it is not sphaira if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) { NacpStruct hbmenu_nacp; @@ -1950,6 +1958,8 @@ App::~App() { usbHsFsExit(); } + log_write("\t[EXIT] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); + if (App::GetLogEnable()) { log_write("closing log\n"); log_file_exit(); diff --git a/sphaira/source/image.cpp b/sphaira/source/image.cpp index 41b2173..47a683d 100644 --- a/sphaira/source/image.cpp +++ b/sphaira/source/image.cpp @@ -20,7 +20,9 @@ #pragma GCC diagnostic pop #pragma GCC diagnostic pop +#include "app.hpp" #include "log.hpp" +#include #include namespace sphaira { @@ -30,7 +32,6 @@ constexpr int BPP = 4; auto ImageLoadInternal(stbi_uc* image_data, int x, int y) -> ImageResult { if (image_data) { - log_write("loaded image: w: %d h: %d\n", x, y); ImageResult result{}; result.data.resize(x*y*BPP); result.w = x; @@ -44,17 +45,63 @@ auto ImageLoadInternal(stbi_uc* image_data, int x, int y) -> ImageResult { return {}; } -} // namespace +auto ImageLoadInternal(nj::Image&& image) -> ImageResult { + if (!image.is_valid() || image.parse()) { + log_write("[NVJPG] failed to parse image\n"); + return {}; + } -auto ImageLoadFromMemory(std::span data) -> ImageResult { - int x, y, channels; - return ImageLoadInternal(stbi_load_from_memory(data.data(), data.size(), &x, &y, &channels, BPP), x, y); + nj::Surface surf{image.width, image.height}; + if (surf.allocate()) { + log_write("[NVJPG] failed to allocate surf\n"); + return {}; + } + + if (R_FAILED(App::GetApp()->m_decoder.render(image, surf, 255))) { + log_write("[NVJPG] failed to render\n"); + return {}; + } + + if (R_FAILED(App::GetApp()->m_decoder.wait(surf))) { + log_write("[NVJPG] failed to wait\n"); + return {}; + } + + ImageResult result{}; + result.w = image.width; + result.h = image.height; + result.data.resize(surf.size()); + std::memcpy(result.data.data(), surf.data(), result.data.size()); + + return result; } -auto ImageLoadFromFile(const fs::FsPath& file) -> ImageResult { - log_write("doing file load\n"); - int x, y, channels; - return ImageLoadInternal(stbi_load(file, &x, &y, &channels, BPP), x, y); +} // namespace + +auto ImageLoadFromMemory(std::span data, u32 flags) -> ImageResult { + if (flags & ImageFlag_JPEG) { + auto shared_vec = std::make_shared>(data.size()); + std::memcpy(shared_vec->data(), data.data(), shared_vec->size()); + // don't make const as it prevents RTO. + auto result = ImageLoadInternal(nj::Image{shared_vec}); + // if it failed, try again but without using oss-jpg. + return result.data.empty() ? ImageLoadFromMemory(data, 0) : result; + } else { + int x, y, channels; + return ImageLoadInternal(stbi_load_from_memory(data.data(), data.size(), &x, &y, &channels, BPP), x, y); + } +} + +auto ImageLoadFromFile(const fs::FsPath& file, u32 flags) -> ImageResult { + if (flags & ImageFlag_JPEG) { + // don't make const as it prevents RTO. + auto result = ImageLoadInternal(nj::Image{file}); + // if it failed, try again but without using oss-jpg. + return result.data.empty() ? ImageLoadFromFile(file, 0) : result; + } else { + int x, y, channels; + return ImageLoadInternal(stbi_load(file, &x, &y, &channels, BPP), x, y); + } } auto ImageResize(std::span data, int inx, int iny, int outx, int outy) -> ImageResult { diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index 44e2e28..3399b7e 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -4,6 +4,7 @@ #include "dumper.hpp" #include "defines.hpp" #include "i18n.hpp" +#include "image.hpp" #include "ui/menus/game_menu.hpp" #include "ui/sidebar.hpp" @@ -318,11 +319,15 @@ void FakeNacpEntry(ThreadResultData& e) { bool LoadControlImage(Entry& e) { if (!e.image && e.control) { + ON_SCOPE_EXIT(e.control.reset()); + TimeStamp ts; - e.image = nvgCreateImageMem(App::GetVg(), 0, e.control->icon, e.jpeg_size); - e.control.reset(); - log_write("\t\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); - return true; + const auto image = ImageLoadFromMemory({e.control->icon, e.jpeg_size}, ImageFlag_JPEG); + if (!image.data.empty()) { + e.image = nvgCreateImageRGBA(App::GetVg(), image.w, image.h, 0, image.data.data()); + log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); + return true; + } } return false; diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp index 898a6b1..06fa0d1 100644 --- a/sphaira/source/ui/menus/gc_menu.cpp +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -13,6 +13,7 @@ #include "i18n.hpp" #include "download.hpp" #include "dumper.hpp" +#include "image.hpp" #include #include @@ -965,7 +966,13 @@ void Menu::OnChangeIndex(s64 new_index) { const auto& e = m_entries[m_entry_index]; const auto jpeg_size = e.control_size - sizeof(NacpStruct); - m_icon = nvgCreateImageMem(App::GetVg(), 0, e.control->icon, jpeg_size); + + TimeStamp ts; + const auto image = ImageLoadFromMemory({e.control->icon, jpeg_size}, ImageFlag_JPEG); + if (!image.data.empty()) { + m_icon = nvgCreateImageRGBA(App::GetVg(), image.w, image.h, 0, image.data.data()); + log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); + } } } diff --git a/sphaira/source/ui/menus/homebrew.cpp b/sphaira/source/ui/menus/homebrew.cpp index a5bb566..d4ae80d 100644 --- a/sphaira/source/ui/menus/homebrew.cpp +++ b/sphaira/source/ui/menus/homebrew.cpp @@ -10,6 +10,7 @@ #include "owo.hpp" #include "defines.hpp" #include "i18n.hpp" +#include "image.hpp" #include #include @@ -175,9 +176,17 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { // really, switch-tools should handle this by resizing the image before // adding it to the nro, as well as validate its a valid jpeg. const auto icon = nro_get_icon(e.path, e.icon_size, e.icon_offset); + TimeStamp ts; if (!icon.empty()) { - e.image = nvgCreateImageMem(vg, 0, icon.data(), icon.size()); - image_load_count++; + const auto image = ImageLoadFromMemory(icon, ImageFlag_JPEG); + if (!image.data.empty()) { + e.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, image.data.data()); + log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); + image_load_count++; + } else { + // prevent loading of this icon again as it's already failed. + e.icon_offset = e.icon_size = 0; + } } } } diff --git a/sphaira/source/ui/menus/themezer.cpp b/sphaira/source/ui/menus/themezer.cpp index 934ce57..25a2d7d 100644 --- a/sphaira/source/ui/menus/themezer.cpp +++ b/sphaira/source/ui/menus/themezer.cpp @@ -12,6 +12,7 @@ #include "swkbd.hpp" #include "i18n.hpp" #include "threaded_file_transfer.hpp" +#include "image.hpp" #include #include @@ -134,19 +135,14 @@ auto loadThemeImage(ThemeEntry& e) -> bool { } auto vg = App::GetVg(); - fs::FsNativeSd fs; - std::vector image_buf; - const auto path = apiBuildIconCache(e); - if (R_FAILED(fs.read_entire_file(path, image_buf))) { - log_write("failed to load image from file: %s\n", path.s); - } else { - int channels_in_file; - auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4); - if (buf) { - ON_SCOPE_EXIT(stbi_image_free(buf)); - image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf); - } + TimeStamp ts; + const auto data = ImageLoadFromFile(path, ImageFlag_JPEG); + if (!data.data.empty()) { + image.w = data.w; + image.h = data.h; + image.image = nvgCreateImageRGBA(vg, data.w, data.h, 0, data.data.data()); + log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); } if (!image.image) {