diff --git a/CMakeLists.txt b/CMakeLists.txt index b133245..f558e03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,11 +34,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE src/) # make spng use miniz instead of zlib target_compile_definitions(${PROJECT_NAME} PRIVATE SPNG_USE_MINIZ=1 SPNG_STATIC=1) -if (BLAZE_DEBUG) - target_compile_definitions(${PROJECT_NAME} PRIVATE BLAZE_DEBUG=1) -endif() - -CPMAddPackage("gh:dankmeme01/asp2#7024af9") +CPMAddPackage("gh:dankmeme01/asp2#8e25afa") CPMAddPackage( NAME Boost VERSION 1.84.0 @@ -54,6 +50,12 @@ CPMAddPackage( OPTIONS "LIBDEFLATE_BUILD_SHARED_LIB OFF" "LIBDEFLATE_BUILD_GZIP OFF" ) +if (BLAZE_DEBUG) + target_compile_definitions(${PROJECT_NAME} PRIVATE BLAZE_DEBUG=1) + target_compile_definitions(asp PRIVATE _HAS_ITERATOR_DEBUGGING=0) + target_compile_definitions(simdutf PRIVATE _HAS_ITERATOR_DEBUGGING=0) +endif() + # Suppress some warnings if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_options(boost_container PRIVATE "-Wno-incompatible-pointer-types") diff --git a/README.md b/README.md index 9de9a9e..5b1d015 100644 --- a/README.md +++ b/README.md @@ -24,5 +24,5 @@ Note: all those numbers are on Windows. On other operating systems, the performa ## Credit -* [matcool](https://github.com/matcool) for [Fast Format](https://github.com/matcool/geode-mods/blob/main/fast-format/main.cpp) -* [cgytrus](https://github.com/cgytrus) for [Algebra Dash](https://github.com/cgytrus/AlgebraDash) +* [matcool](https://github.com/matcool) for [Fast Format](https://github.com/matcool/geode-mods/blob/main/fast-format/main.cpp) (i took the entire mod) +* [cgytrus](https://github.com/cgytrus) for [Algebra Dash](https://github.com/cgytrus/AlgebraDash) (Tracy integration taken from it, the rest of the optimizations were implemented by myself, even if the ideas are similar to ones in Config's mod) diff --git a/src/algo/compress.cpp b/src/algo/compress.cpp index a428fd0..7c05372 100644 --- a/src/algo/compress.cpp +++ b/src/algo/compress.cpp @@ -92,7 +92,7 @@ namespace blaze { } Result Decompressor::decompressToChunk(const void* input, size_t size) { - OwnedMemoryChunk chunk(size * 2); + OwnedMemoryChunk chunk(size * 3); size_t writtenSize; diff --git a/src/ccimageext.cpp b/src/ccimageext.cpp index 0d3058f..e830660 100644 --- a/src/ccimageext.cpp +++ b/src/ccimageext.cpp @@ -45,23 +45,23 @@ void CCImageExt::initWithDecodedImage(DecodedImage& img) { } } -Result<> CCImageExt::initWithSPNG(void* data, size_t size) { - GEODE_UNWRAP_INTO(auto img, blaze::decodeSPNG(static_cast(data), size)); +Result<> CCImageExt::initWithSPNG(const void* data, size_t size) { + GEODE_UNWRAP_INTO(auto img, blaze::decodeSPNG(static_cast(data), size)); this->initWithDecodedImage(img); return Ok(); } -Result<> CCImageExt::initWithFPNG(void* data, size_t size) { - GEODE_UNWRAP_INTO(auto img, blaze::decodeFPNG(static_cast(data), size)); +Result<> CCImageExt::initWithFPNG(const void* data, size_t size) { + GEODE_UNWRAP_INTO(auto img, blaze::decodeFPNG(static_cast(data), size)); this->initWithDecodedImage(img); return Ok(); } -Result<> CCImageExt::initWithSPNGOrCache(uint8_t* buffer, size_t size, const char* imgPath) { +Result<> CCImageExt::initWithSPNGOrCache(const uint8_t* buffer, size_t size, const char* imgPath) { ZoneScoped; // check if we have cached it already @@ -106,4 +106,8 @@ Result<> CCImageExt::initWithSPNGOrCache(uint8_t* buffer, size_t size, const cha return this->initWithSPNG(buffer, size); } +Result<> CCImageExt::initWithSPNGOrCache(const blaze::OwnedMemoryChunk& chunk, const char* imgPath) { + return this->initWithSPNGOrCache(chunk.data, chunk.size, imgPath); +} + } \ No newline at end of file diff --git a/src/ccimageext.hpp b/src/ccimageext.hpp index 24474d6..26dfbc6 100644 --- a/src/ccimageext.hpp +++ b/src/ccimageext.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace blaze { @@ -14,9 +15,10 @@ class CCImageExt : public cocos2d::CCImage { void setImageProperties(uint32_t width, uint32_t height, uint32_t bitDepth, bool hasAlpha, bool hasPreMulti); - geode::Result<> initWithSPNG(void* data, size_t size); - geode::Result<> initWithFPNG(void* data, size_t size); - geode::Result<> initWithSPNGOrCache(uint8_t* data, size_t size, const char* imgPath); + geode::Result<> initWithSPNG(const void* data, size_t size); + geode::Result<> initWithFPNG(const void* data, size_t size); + geode::Result<> initWithSPNGOrCache(const uint8_t* data, size_t size, const char* imgPath); + geode::Result<> initWithSPNGOrCache(const blaze::OwnedMemoryChunk& chunk, const char* imgPath); private: void initWithDecodedImage(DecodedImage&); diff --git a/src/hooks/LoadingLayer.cpp b/src/hooks/LoadingLayer.cpp index 78022bd..0c59322 100644 --- a/src/hooks/LoadingLayer.cpp +++ b/src/hooks/LoadingLayer.cpp @@ -1,8 +1,10 @@ // Blaze LoadingLayer hooks. // -// Completely rewrites `LoadingLayer::loadAssets` to be multi-threaded. +// Completely rewrites the loading logic to be multithreaded. #include +#include +#include #include #include @@ -15,6 +17,10 @@ #include using namespace geode::prelude; +using hclock = std::chrono::high_resolution_clock; + +// mutexes for thread unsafe classes +static asp::Mutex<> texCacheMutex, sfcacheMutex; // big hack to call a private cocos function namespace { @@ -34,129 +40,328 @@ namespace { void _addSpriteFramesWithDictionary(CCDictionary* p1, CCTexture2D* p2); } -struct MTTextureInitTask { - std::string sheetName; - CCImage* img; - const char* plistToLoad = nullptr; - std::string plistFullPathToLoad; -}; +// AsyncImageLoadRequest - structure that handles the loading of a specific texture +namespace { +struct AsyncImageLoadRequest { + const char* pngFile = nullptr; + const char* plistFile = nullptr; + gd::string pathKey; + blaze::OwnedMemoryChunk imageData{}; + Ref image = nullptr; + Ref texture = nullptr; + + AsyncImageLoadRequest(const char* pngFile, const char* plistFile) : pngFile(pngFile), plistFile(plistFile) {} + AsyncImageLoadRequest(const char* pngFile) : pngFile(pngFile), plistFile(nullptr) {} + + AsyncImageLoadRequest(const AsyncImageLoadRequest&) = delete; + AsyncImageLoadRequest& operator=(const AsyncImageLoadRequest&) = delete; + + AsyncImageLoadRequest(AsyncImageLoadRequest&& other) { + this->pngFile = other.pngFile; + this->plistFile = other.plistFile; + this->pathKey = std::move(other.pathKey); + this->imageData = std::move(other.imageData); + this->image = std::move(other.image); + this->texture = std::move(other.texture); + + other.pngFile = nullptr; + other.plistFile = nullptr; + } + + AsyncImageLoadRequest& operator=(AsyncImageLoadRequest&& other) { + if (this != &other) { + this->pngFile = other.pngFile; + this->plistFile = other.plistFile; + this->pathKey = std::move(other.pathKey); + this->imageData = std::move(other.imageData); + this->image = std::move(other.image); + this->texture = std::move(other.texture); + + other.pngFile = nullptr; + other.plistFile = nullptr; + } -static asp::Mutex<> texCacheMutex; -static asp::Mutex<> sfcacheMutex; + return *this; + } -static std::optional asyncLoadImage(const char* sheet, const char* plistToLoad) { - ZoneScoped; + // Loads the encoded image data into memory. Does nothing if the image is already loaded. + inline Result<> loadImage() { + ZoneScoped; + + if (imageData) return Ok(); + + this->pathKey = CCFileUtils::get()->fullPathForFilename(pngFile, false); + if (pathKey.empty()) { + return Err(fmt::format("Failed to find path for image {}", pngFile)); + } + + this->imageData = LoadManager::get().readFileToChunk(pathKey.c_str()); + + if (!imageData || imageData.size == 0) { + return Err(fmt::format("Failed to open image file at {}", pathKey)); + } - auto pathKey = CCFileUtils::get()->fullPathForFilename(sheet, false); - if (pathKey.empty()) { - log::warn("Failed to find: {}", sheet); - return std::nullopt; + return Ok(); } - auto _l = texCacheMutex.lock(); - if (CCTextureCache::get()->m_pTextures->objectForKey(pathKey)) { - log::warn("Already loaded: {}", pathKey); - return std::nullopt; + bool isImageLoaded() { + return this->imageData; } - _l.unlock(); - size_t dataSize; - auto data = LoadManager::get().readFile(pathKey.c_str(), dataSize); + inline Result<> initImage() { + ZoneScoped; + + if (!imageData) return Err("attempting to initialize an image that hasn't been loaded yet"); + + this->image = new CCImage(); + this->image->release(); // make refcount go to 1 + auto ret = static_cast(this->image.data())->initWithSPNGOrCache(imageData, pathKey.c_str()); + + if (!ret) { + this->image = nullptr; + return Err(fmt::format("Failed to load image: {}", ret.unwrapErr())); + } + + return Ok(); + } - if (!data || dataSize == 0) { - log::warn("Failed to open: {}", pathKey); - return std::nullopt; + bool isImageInitialized() { + return this->image != nullptr; } - auto img = new CCImage(); - auto ret = static_cast(img)->initWithSPNGOrCache(data.get(), dataSize, pathKey.c_str()); + // Initializes the opengl texture from the loaded image. Must be called on the main thread + inline Result<> initTexture() { + ZoneScoped; - if (!ret) { - img->release(); - log::error("Failed to load image {}: {}", sheet, ret.unwrapErr()); - return std::nullopt; + if (!image) return Err("attempting to initialize a texture before initializing an image"); + + this->texture = new CCTexture2D(); + this->texture->release(); // make refcount go to 1 + + if (!texture->initWithImage(image)) { + this->image = nullptr; + this->texture = nullptr; + return Err("failed to initialize cctexture2d");; + } + + auto _lck = texCacheMutex.lock(); + CCTextureCache::get()->m_pTextures->setObject(texture, pathKey); + + return Ok(); } - std::string_view pksv(pathKey); + bool isTextureLoaded() { + return this->texture != nullptr; + } - return MTTextureInitTask { - .sheetName = std::move(pathKey), - .img = img, - .plistToLoad = plistToLoad, - // oh the horrors - .plistFullPathToLoad = plistToLoad ? (std::string(pksv.substr(0, pksv.find(".png"))) + ".plist") : "" - }; -} + // Adds sprite frames given the plist file. Does nothing if plist file is nullptr. + inline void addSpriteFramesSync() const { + if (!plistFile) return; + if (!texture) return; + + auto _g = sfcacheMutex.lock(); + CCSpriteFrameCache::get()->addSpriteFramesWithFile(plistFile, texture); + } + + // Async version of addSpriteFramesSync + inline void addSpriteFramesAsync(TextureQuality texQuality) const { + if (!plistFile) return; + if (!texture) return; -static void asyncAddSpriteFrames(const char* fullPlistGuess, const char* plist, CCTexture2D* texture, const std::string& sheetName) { - ZoneScoped; + ZoneScoped; + + // try to guess the full plist name without calling fullPathForFilename + auto pfsv = std::string_view(plistFile); + + // strip .plist extension + std::string plistPath; + plistPath.reserve(pfsv.size() - sizeof(".plist") + sizeof("-uhd.plist")); + plistPath.append(pfsv.substr(0, pfsv.size() - sizeof(".plist"))); + + switch (texQuality) { + case cocos2d::kTextureQualityLow: + plistPath.append(std::string_view(".plist")); break; + case cocos2d::kTextureQualityMedium: + plistPath.append(std::string_view("-hd.plist")); break; + case cocos2d::kTextureQualityHigh: + plistPath.append(std::string_view("-uhd.plist")); break; + } - CCDictionary* dict; + CCDictionary* dict; - { - ZoneScopedN("asyndAddSpriteFrames ccdict"); + { + ZoneScopedN("asyndAddSpriteFrames ccdict"); - dict = CCDictionary::createWithContentsOfFileThreadSafe(fullPlistGuess); + dict = CCDictionary::createWithContentsOfFileThreadSafe(plistPath.c_str()); - if (!dict) { - dict = CCDictionary::createWithContentsOfFileThreadSafe(CCFileUtils::get()->fullPathForFilename(plist, false).c_str()); + if (!dict) { + plistPath = CCFileUtils::get()->fullPathForFilename(plistFile, false); + dict = CCDictionary::createWithContentsOfFileThreadSafe(plistPath.c_str()); + } + + if (!dict) { + log::warn("failed to find the plist for {}", plistFile); + auto _mtx = texCacheMutex.lock(); + CCTextureCache::get()->m_pTextures->removeObjectForKey(pathKey); + return; + } } - if (!dict) { - log::warn("failed to find the plist for {}", plist); - auto _mtx = texCacheMutex.lock(); - CCTextureCache::get()->m_pTextures->removeObjectForKey(sheetName); - return; + { + ZoneScopedN("addSpriteFramesAsync call to addSpriteFramesWithDictionary"); + auto _lck = sfcacheMutex.lock(); + _addSpriteFramesWithDictionary(dict, texture); } - } - { - ZoneScopedN("asyncAddSpriteFrames addSpriteFrames"); - auto _lastlck = sfcacheMutex.lock(); - _addSpriteFramesWithDictionary(dict, texture); - _lastlck.unlock(); + dict->release(); } +}; +} - dict->release(); +#define MAKE_SHEET(name) textures.push_back(AsyncImageLoadRequest { name".png", name".plist" }) +#define MAKE_IMG(name) textures.push_back(AsyncImageLoadRequest { name".png" }) +#define MAKE_FONT(name) do { \ + auto conf = FNTConfigLoadFile(name".fnt"); \ + if (conf) textures.push_back(AsyncImageLoadRequest { conf->getAtlasName() }); \ + else geode::log::warn("Failed to load font file: " name ".fnt"); \ + } while (0) + +static std::vector getLoadingLayerResources() { + std::vector textures; + MAKE_SHEET("GJ_LaunchSheet"); + MAKE_IMG("game_bg_01_001"); + MAKE_IMG("slidergroove"); + MAKE_IMG("sliderBar"); + MAKE_FONT("goldFont"); + return textures; } -// Note: Releases the image -static CCTexture2D* addTexture(CCImage* image, const gd::string& sheetName) { - ZoneScoped; +static std::vector getGameResources() { + std::vector textures; + MAKE_SHEET("GJ_GameSheet"); + MAKE_SHEET("GJ_GameSheet02"); + MAKE_SHEET("GJ_GameSheet03"); + MAKE_SHEET("GJ_GameSheetEditor"); + MAKE_SHEET("GJ_GameSheet04"); + MAKE_SHEET("GJ_GameSheetGlow"); + + MAKE_SHEET("FireSheet_01"); + MAKE_SHEET("GJ_ShopSheet"); + MAKE_IMG("smallDot"); + MAKE_IMG("square02_001"); + MAKE_SHEET("GJ_ParticleSheet"); + MAKE_SHEET("PixelSheet_01"); + + MAKE_SHEET("CCControlColourPickerSpriteSheet"); + MAKE_IMG("GJ_gradientBG"); + MAKE_IMG("edit_barBG_001"); + MAKE_IMG("GJ_button_01"); + MAKE_IMG("slidergroove2"); + + MAKE_IMG("GJ_square01"); + MAKE_IMG("GJ_square02"); + MAKE_IMG("GJ_square03"); + MAKE_IMG("GJ_square04"); + MAKE_IMG("GJ_square05"); + MAKE_IMG("gravityLine_001"); + + MAKE_FONT("bigFont"); + MAKE_FONT("chatFont"); + return textures; +} +#undef MAKE_SHEET +#undef MAKE_IMG +#undef MAKE_FONT + +static hclock::time_point g_launchTime{}; +static std::optional s_loadThreadPool{}; + +struct PreLoadStageData { + std::optional>> thread; + std::unique_ptr> channel; + + void cleanup() { + thread.reset(); + channel.reset(); + } +}; + +struct GameLoadStageData { + std::vector requests; + std::unique_ptr> channel; - auto texture = new CCTexture2D(); - if (!texture->initWithImage(image)) { - delete texture; - image->release(); - log::warn("failed to init cctexture2d: {}", image); - return nullptr; + void cleanup() { + requests.clear(); + channel.reset(); } +}; - auto _lck = texCacheMutex.lock(); - CCTextureCache::get()->m_pTextures->setObject(texture, sheetName); - _lck.unlock(); +static PreLoadStageData s_preLoadStage; +static GameLoadStageData s_gameLoadStage; + +template +static void asyncLoadLoadingLayerResources(std::vector&& resources) { + asp::Thread> thread; + thread.setLoopFunction([](std::vector& items, auto& stopToken) { + for (auto& item : items) { + Result<> res; + + if constexpr (!SkipLoadImage) { + res = item.loadImage(); + if (!res) { + log::warn("Error loading {}: {}", item.pngFile ? item.pngFile : "", res.unwrapErr()); + return; + } + } - texture->release(); - image->release(); + res = item.initImage(); + if (!res) { + log::warn("Error loading {}: {}", item.pngFile ? item.pngFile : "", res.unwrapErr()); + } else { + s_preLoadStage.channel->push(std::move(item)); + } + } - return texture; + stopToken.stop(); + }); + s_preLoadStage.thread.emplace(std::move(thread)); + s_preLoadStage.channel = std::make_unique>(); + s_preLoadStage.thread->start(std::move(resources)); } -// Note: not thread safe -static CCBMFontConfiguration* loadFontConfiguration(const char* name) { - ZoneScoped; +template +static void asyncLoadGameResources(std::vector&& resources) { + s_gameLoadStage.requests = std::move(resources); + s_gameLoadStage.channel = std::make_unique>(); + + for (auto& img : s_gameLoadStage.requests) { + s_loadThreadPool->pushTask([&img] { + Result<> res; + + if constexpr (!SkipLoadImage) { + res = img.loadImage(); + if (!res) { + log::warn("Error loading {}: {}", img.pngFile, res.unwrapErr()); + return; + } + } - // CCLabelBMFont::create(" ", name); - return FNTConfigLoadFile(name); + res = img.initImage(); + if (!res) { + log::warn("Error loading {}: {}", img.pngFile, res.unwrapErr()); + } else { + s_gameLoadStage.channel->push(&img); + } + }); + } } -#include class $modify(MyLoadingLayer, LoadingLayer) { struct Fields { - asp::ThreadPool threadPool{std::thread::hardware_concurrency()}; bool finishedLoading = false; - std::chrono::high_resolution_clock::time_point startedLoadingGame; - std::chrono::high_resolution_clock::time_point startedLoadingAssets; + hclock::time_point startedLoadingGame; + hclock::time_point finishedLoadingGame; + hclock::time_point startedLoadingAssets; }; static void onModify(auto& self) { @@ -164,120 +369,100 @@ class $modify(MyLoadingLayer, LoadingLayer) { BLAZE_HOOK_VERY_LAST(LoadingLayer::init); // idk geode was crashin without this } - // this will eventually get called by geode - $override - void loadAssets() { - if (m_fields->finishedLoading) { - m_loadStep = 14; - LoadingLayer::loadAssets(); - } else { - this->customLoadStep(); + bool init(bool fromReload) { + m_fields->startedLoadingGame = hclock::now(); + + BLAZE_TIMER_START("(LoadingLayer::init) Initial setup"); + + if (!CCLayer::init()) return false; + + this->m_fromRefresh = fromReload; + CCDirector::get()->m_bDisplayStats = true; + + if (fromReload) { + // Load loadinglayer assets + asyncLoadLoadingLayerResources(getLoadingLayerResources()); } - } - void customLoadStep() { - ZoneScoped; + // FMOD is setup here on android +#ifdef GEODE_IS_ANDROID + if (!fromReload) { + FMODAudioEngine::get()->setup(); + } +#endif - BLAZE_TIMER_START("(customLoadStep) Task queueing"); + auto* gm = GameManager::get(); + if (gm->m_switchModes) { + gm->m_switchModes = false; + GameLevelManager::get()->getLevelSaveData(); + } - m_fields->startedLoadingAssets = std::chrono::high_resolution_clock::now(); + // Initialize textures + BLAZE_TIMER_STEP("Main thread tasks"); - auto tcache = CCTextureCache::get(); - auto sfcache = CCSpriteFrameCache::get(); + auto* sfcache = CCSpriteFrameCache::get(); - struct LoadSheetTask { - const char *sheet, *plist; - }; + while (true) { + if (s_preLoadStage.channel->empty()) { + if (!s_preLoadStage.thread->isStopped()) { + std::this_thread::yield(); + continue; + } else if (s_preLoadStage.channel->empty()) { + break; + } + } - struct LoadImageTask { - const char *image; - }; + auto iTask = s_preLoadStage.channel->popNow(); + auto res = iTask.initTexture(); + if (!res) { + log::warn("Failed to init texture for {}: {}", iTask.pngFile, res.unwrapErr()); + continue; + } - struct LoadFontTask { - CCBMFontConfiguration* configuration; - }; + iTask.addSpriteFramesSync(); + } - struct LoadCustomTask { - std::function func; - }; + BLAZE_TIMER_STEP("LoadingLayer UI"); + + this->addLoadingLayerUi(); + + auto* acm = CCDirector::get()->getActionManager(); + auto action = CCSequence::create( + CCDelayTime::create(0.f), + CCCallFunc::create(this, callfunc_selector(MyLoadingLayer::customLoadStep)), + nullptr + ); - using PreloadTask = std::variant; + acm->addAction(action, this, false); - using MainThreadTask = std::variant; + m_fields->finishedLoadingGame = hclock::now(); - std::vector tasks; - std::vector mtTasks; + BLAZE_TIMER_END(); -#define MAKE_LOADSHEET(name) tasks.push_back(LoadSheetTask { .sheet = name".png", .plist = name".plist" }) -#define MAKE_LOADIMG(name) tasks.push_back(LoadImageTask { .image = name".png" }) -#define MAKE_LOADFONT(name) tasks.push_back(LoadFontTask { .configuration = loadFontConfiguration(name) }) -#define MAKE_LOADCUSTOM(ft) tasks.push_back(LoadCustomTask { .func = [&] ft }) + return true; + } - MAKE_LOADSHEET("GJ_GameSheet"); - MAKE_LOADSHEET("GJ_GameSheet02"); - MAKE_LOADSHEET("GJ_GameSheet03"); - MAKE_LOADSHEET("GJ_GameSheetEditor"); - MAKE_LOADSHEET("GJ_GameSheet04"); - MAKE_LOADSHEET("GJ_GameSheetGlow"); + void customLoadStep() { + ZoneScoped; - MAKE_LOADSHEET("FireSheet_01"); - MAKE_LOADSHEET("GJ_ShopSheet"); - MAKE_LOADIMG("smallDot"); - MAKE_LOADIMG("square02_001"); - MAKE_LOADSHEET("GJ_ParticleSheet"); - MAKE_LOADSHEET("PixelSheet_01"); + BLAZE_TIMER_START("(customLoadStep) Task queueing"); - MAKE_LOADSHEET("CCControlColourPickerSpriteSheet"); - MAKE_LOADIMG("GJ_gradientBG"); - MAKE_LOADIMG("edit_barBG_001"); - MAKE_LOADIMG("GJ_button_01"); - MAKE_LOADIMG("slidergroove2"); + m_fields->startedLoadingAssets = hclock::now(); - MAKE_LOADIMG("GJ_square01"); - MAKE_LOADIMG("GJ_square02"); - MAKE_LOADIMG("GJ_square03"); - MAKE_LOADIMG("GJ_square04"); - MAKE_LOADIMG("GJ_square05"); - MAKE_LOADIMG("gravityLine_001"); + auto tcache = CCTextureCache::get(); + auto sfcache = CCSpriteFrameCache::get(); + + // If it's a refresh, queue all resources to be loaded - MAKE_LOADCUSTOM({ - ZoneScopedN("ObjectToolbox::sharedState"); + if (m_fromRefresh) { + auto resources = getGameResources(); + asyncLoadGameResources(std::move(resources)); + } + s_loadThreadPool->pushTask([] { ObjectToolbox::sharedState(); }); - MAKE_LOADFONT("chatFont.fnt"); - MAKE_LOADFONT("bigFont.fnt"); - - // Push all the threaded tasks to the threadpool - static asp::Channel mainThreadQueue; - - for (auto& task : tasks) { - m_fields->threadPool.pushTask([&, task = std::move(task)] { - // do stuff.. - std::visit(makeVisitor { - [&](const LoadSheetTask& task) { - if (auto res = asyncLoadImage(task.sheet, task.plist)) { - mainThreadQueue.push(std::move(res.value())); - } - }, - [&](const LoadImageTask& task) { - if (auto res = asyncLoadImage(task.image, nullptr)) { - mainThreadQueue.push(std::move(res.value())); - } - }, - [&](const LoadFontTask& task) { - if (auto res = asyncLoadImage(task.configuration->getAtlasName(), nullptr)) { - mainThreadQueue.push(std::move(res.value())); - } - }, - [&](const LoadCustomTask& task) { - task.func(); - }, - }, task); - }); - } - BLAZE_TIMER_STEP("ObjectManager::setup"); ObjectManager::instance()->setup(); @@ -312,139 +497,83 @@ class $modify(MyLoadingLayer, LoadingLayer) { BLAZE_TIMER_STEP("Main thread tasks"); while (true) { - if (mainThreadQueue.empty()) { - if (m_fields->threadPool.isDoingWork()) { + if (s_gameLoadStage.channel->empty()) { + if (!s_loadThreadPool->isDoingWork()) { std::this_thread::yield(); continue; - } else if (mainThreadQueue.empty()) { + } else if (s_gameLoadStage.channel->empty()) { break; } } - auto thing = mainThreadQueue.popNow(); - std::visit(makeVisitor { - [&](const MTTextureInitTask& task) { - ZoneScopedN("Texture init task"); - - CCTexture2D* texture = addTexture(task.img, task.sheetName); - if (!texture) { - return; - } - - if (task.plistToLoad) { - // adding sprite frames - m_fields->threadPool.pushTask([ - texture, - plist = task.plistToLoad, - fullPlist = task.plistFullPathToLoad, - sheetName = task.sheetName - ] { - asyncAddSpriteFrames(fullPlist.c_str(), plist, texture, sheetName); - }); - } - }, - [&](const LoadCustomTask& task) { - ZoneScopedN("Custom task"); - - task.func(); - } - }, thing); - } + auto iTask = s_gameLoadStage.channel->popNow(); + auto res = iTask->initTexture(); + if (!res) { + log::warn("Failed to init texture for {}: {}", iTask->pngFile, res.unwrapErr()); + continue; + } - m_fields->threadPool.join(); + if (iTask->plistFile) { + s_loadThreadPool->pushTask([iTask] { + iTask->addSpriteFramesAsync((TextureQuality) GameManager::get()->m_texQuality); + }); + } + } - CCTextInputNode::create(200.f, 50.f, "Temp", "Thonburi", 0x18, "bigFont.fnt"); + BLAZE_TIMER_STEP("Wait for sprite frames"); - BLAZE_TIMER_STEP("Ensure FMOD is initialized & final tests"); + // also ensure fmod is initialized auto fae = HookedFMODAudioEngine::get(); - { std::lock_guard lock(fae->m_fields->initMutex); } - BLAZE_TIMER_END(); - - auto tookTimeFull = std::chrono::high_resolution_clock::now() - m_fields->startedLoadingGame; - auto tookTimeAssets = std::chrono::high_resolution_clock::now() - m_fields->startedLoadingAssets; - log::info("Loading took {} (out of which assets were {}), handing off..", formatDuration(tookTimeFull), formatDuration(tookTimeAssets)); + s_loadThreadPool->join(); - this->finishLoading(); - } - - void finishLoading() { - m_fields->finishedLoading = true; - this->loadAssets(); - } + BLAZE_TIMER_STEP("Final cleanup"); - // init reimpl - bool init(bool fromReload) { - m_fields->startedLoadingGame = std::chrono::high_resolution_clock::now(); - - BLAZE_TIMER_START("(LoadingLayer::init) Initial setup"); - - if (!CCLayer::init()) return false; - - this->m_fromRefresh = fromReload; - CCDirector::get()->m_bDisplayStats = true; - CCTexture2D::setDefaultAlphaPixelFormat(cocos2d::kCCTexture2DPixelFormat_Default); - - // load the launchsheet and bg in another thread, as fmod setup takes a little bit, and image loading can be parallelized. - asp::Thread<> sheetThread; - - asp::Channel initTasks; - sheetThread.setLoopFunction([&](asp::StopToken<>& stopToken) { -#define ASYNC_IMG(name) if (auto _t = asyncLoadImage(name, nullptr)) { initTasks.push(std::move(_t.value())); } -#define ASYNC_SHEET(name, plist) if (auto _t = asyncLoadImage(name, plist)) { initTasks.push(std::move(_t.value())); } - - ASYNC_SHEET("GJ_LaunchSheet.png", "GJ_LaunchSheet.plist"); - ASYNC_IMG("game_bg_01_001.png"); - ASYNC_IMG("slidergroove.png"); - ASYNC_IMG("sliderBar.png"); - -#undef ASYNC_IMG - - stopToken.stop(); - }); + CCTextInputNode::create(200.f, 50.f, "Temp", "Thonburi", 0x18, "bigFont.fnt"); - sheetThread.start(); + // cleanup in another thread because it can block for a few ms + std::thread([] { + s_preLoadStage.cleanup(); + s_gameLoadStage.cleanup(); + s_loadThreadPool.reset(); + }).detach(); - // FMOD setup (asynchronous on Windows) + BLAZE_TIMER_END(); - if (!fromReload) { - FMODAudioEngine::get()->setup(); - } + this->finishLoading(); + } - auto* gm = GameManager::get(); - if (gm->m_switchModes) { - gm->m_switchModes = false; - GameLevelManager::get()->getLevelSaveData(); + $override + void loadAssets() { + if (m_fields->finishedLoading) { + m_loadStep = 14; + LoadingLayer::loadAssets(); + } else { + this->customLoadStep(); } + } - // Continue loading launchsheet - BLAZE_TIMER_STEP("Launchsheet loading"); - - auto* sfcache = CCSpriteFrameCache::get(); - - while (true) { - if (initTasks.empty()) { - if (!sheetThread.isStopped()) { - std::this_thread::yield(); - continue; - } else if (initTasks.empty()) { - break; - } - } + void finishLoading() { + auto finishTime = hclock::now(); - auto iTask = initTasks.popNow(); + auto tookTimeFull = finishTime - g_launchTime; + log::info("Loading took {}, handing off..", formatDuration(tookTimeFull)); - addTexture(iTask.img, iTask.sheetName); - if (iTask.plistToLoad) { - sfcache->addSpriteFramesWithFile(iTask.plistToLoad); - } - } +#ifdef BLAZE_DEBUG + log::debug("- Initial game setup: {}", formatDuration(m_fields->startedLoadingGame - g_launchTime)); + log::debug("- Pre-loading: {}", formatDuration(m_fields->finishedLoadingGame - m_fields->startedLoadingGame)); + log::debug("- Asset loading: {}", formatDuration(finishTime - m_fields->startedLoadingAssets)); +#endif - BLAZE_TIMER_STEP("LoadingLayer UI"); + m_fields->finishedLoading = true; + this->loadAssets(); + } + // Boring stuff + void addLoadingLayerUi() { auto winSize = CCDirector::get()->getWinSize(); auto bg = CCSprite::create("game_bg_01_001.png"); @@ -516,17 +645,85 @@ class $modify(MyLoadingLayer, LoadingLayer) { groove->setPosition({m_caption->getPosition().x, txareaPos.y + 40.f}); this->updateProgress(0); - auto* acm = CCDirector::get()->getActionManager(); - auto action = CCSequence::create( - CCDelayTime::create(0.f), - CCCallFunc::create(this, callfunc_selector(MyLoadingLayer::customLoadStep)), - nullptr - ); + } +}; - acm->addAction(action, this, false); +class $modify(CCApplication) { + int run() override { + BLAZE_TIMER_START("CCApplication::run (managers pre-setup)"); + g_launchTime = hclock::now(); + + CCFileUtils::get()->addSearchPath("Resources"); + + // initialize gamemanager, we have to do it on main thread right here + GameManager::get(); + + // setup fmod asynchronously +#ifndef GEODE_IS_ANDROID + FMODAudioEngine::get()->setup(); +#endif + + // initialize llm in another thread. + // we can only do this if we carefully checked that all the functions afterwards are thread-safe and do not call autorelease + asp::Thread<> llmLoadThread; + llmLoadThread.setLoopFunction([](auto& stopToken) { + LocalLevelManager::get(); + stopToken.stop(); + }); + llmLoadThread.start(); + + // set appropriate texture quality + auto tq = GameManager::get()->m_texQuality; + CCDirector::get()->updateContentScale((TextureQuality)tq); + CCTexture2D::setDefaultAlphaPixelFormat(cocos2d::kCCTexture2DPixelFormat_Default); + + BLAZE_TIMER_STEP("preparation for asset preloading"); + + // Init threadpool + s_loadThreadPool.emplace(asp::ThreadPool{}); + + auto resources1 = getLoadingLayerResources(); + auto resources2 = getGameResources(); + + // Load images + + for (auto& img : resources1) { + s_loadThreadPool->pushTask([&img] { + auto res = img.loadImage(); + if (!res) { + log::warn("Failed to initialize image: {}", res.unwrapErr()); + } + }); + } + + for (auto& img : resources2) { + s_loadThreadPool->pushTask([&img] { + auto res = img.loadImage(); + if (!res) { + log::warn("Failed to initialize image: {}", res.unwrapErr()); + } + }); + } + + // wait for all images to be preloaded (should be pretty quick, they are not decoded yet) + s_loadThreadPool->join(); + + // start decoding the images in background + asyncLoadLoadingLayerResources(std::move(resources1)); + + // rest of the images (for the game itself) + asyncLoadGameResources(std::move(resources2)); + + CCFileUtils::get()->removeSearchPath("Resources"); + + // wait until llm finishes initialization + BLAZE_TIMER_STEP("Wait for LLM to finish"); + llmLoadThread.join(); BLAZE_TIMER_END(); - return true; + // finally go back to running the rest of the game + + return CCApplication::run(); } -}; \ No newline at end of file +}; diff --git a/src/main.cpp b/src/main.cpp index 45e5dfa..72194c2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,4 +6,4 @@ using namespace geode::prelude; $execute { fpng::fpng_init(); -} \ No newline at end of file +} diff --git a/src/manager.cpp b/src/manager.cpp index c71bde6..b77505d 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -79,6 +79,17 @@ std::unique_ptr LoadManager::readFile(const char* path, size_t& outSi return std::unique_ptr(buf); } +blaze::OwnedMemoryChunk LoadManager::readFileToChunk(const char* path) { + size_t outSize; + auto ptr = this->readFile(path, outSize); + + if (ptr) { + return blaze::OwnedMemoryChunk{std::move(ptr), outSize}; + } else { + return {}; + } +} + void LoadManager::threadFunc(decltype(converterThread)::StopToken& st) { auto tasko = converterQueue.popTimeout(std::chrono::seconds(1)); diff --git a/src/manager.hpp b/src/manager.hpp index 5c95bcb..cd3af85 100644 --- a/src/manager.hpp +++ b/src/manager.hpp @@ -14,6 +14,7 @@ class LoadManager : public SingletonBase { std::filesystem::path getCacheDir(); std::filesystem::path cacheFileForChecksum(uint32_t crc); std::unique_ptr readFile(const char* path, size_t& outSize); + blaze::OwnedMemoryChunk readFileToChunk(const char* path); private: diff --git a/src/tracing.cpp b/src/tracing.cpp index c6d4776..2e6745b 100644 --- a/src/tracing.cpp +++ b/src/tracing.cpp @@ -1,6 +1,7 @@ #include "tracing.hpp" #ifdef BLAZE_TRACY + #include class GLFWwindow; #include @@ -238,4 +239,7 @@ PROFILER_HOOK(void, CCNode, update, float) PROFILER_HOOK(void, CCNode, visit) PROFILER_HOOK(void, CCNode, updateTransform) +PROFILER_HOOK(int, CCApplication, run) +GEODE_WINDOWS(PROFILER_HOOK(void, AppDelegate, setupGLView)) + #endif // BLAZE_TRACY \ No newline at end of file diff --git a/src/util.cpp b/src/util.cpp index 901a504..0ee7b43 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -40,5 +40,52 @@ void unreachable() { #endif } +OwnedMemoryChunk::~OwnedMemoryChunk() { + std::free(data); +} + +OwnedMemoryChunk::OwnedMemoryChunk(size_t size) : data(reinterpret_cast(std::malloc(size))), size(size) { + if (!this->data) { + throw std::bad_alloc(); + } +} + +OwnedMemoryChunk::OwnedMemoryChunk(uint8_t* data, size_t size) : data(data), size(size) {} +OwnedMemoryChunk::OwnedMemoryChunk(std::unique_ptr ptr, size_t size) : data(ptr.release()), size(size) {} +OwnedMemoryChunk::OwnedMemoryChunk() : data(nullptr), size(0) {} + +std::pair OwnedMemoryChunk::release() { + auto p = std::make_pair(data, size); + + this->data = nullptr; + this->size = 0; + + return p; +} + +void OwnedMemoryChunk::realloc(size_t newSize) { + this->data = reinterpret_cast(std::realloc(data, newSize)); + if (!this->data) { + throw std::bad_alloc(); + } + + this->size = newSize; +} + +OwnedMemoryChunk::OwnedMemoryChunk(OwnedMemoryChunk&& other) { + std::tie(this->data, this->size) = other.release(); +} + +OwnedMemoryChunk& OwnedMemoryChunk::operator=(OwnedMemoryChunk&& other) { + if (this != &other) { + std::tie(this->data, this->size) = other.release(); + } + + return *this; +} + +OwnedMemoryChunk::operator bool() { + return this->data != nullptr; +} } \ No newline at end of file diff --git a/src/util.hpp b/src/util.hpp index 4a96bba..c760d42 100644 --- a/src/util.hpp +++ b/src/util.hpp @@ -94,51 +94,26 @@ namespace blaze { uint8_t* data = nullptr; size_t size; - inline ~OwnedMemoryChunk() { - std::free(data); - } + ~OwnedMemoryChunk(); - inline OwnedMemoryChunk(size_t size) : data(reinterpret_cast(std::malloc(size))), size(size) { - if (!this->data) { - throw std::bad_alloc(); - } - } + OwnedMemoryChunk(size_t size); // Note that this class takes ownership of the passed pointer! - inline OwnedMemoryChunk(uint8_t* data, size_t size) : data(data), size(size) {} - inline OwnedMemoryChunk(std::unique_ptr ptr, size_t size) : data(ptr.release()), size(size) {} + OwnedMemoryChunk(uint8_t* data, size_t size); + OwnedMemoryChunk(std::unique_ptr ptr, size_t size); + OwnedMemoryChunk(); - inline std::pair release() { - auto p = std::make_pair(data, size); + std::pair release(); - this->data = nullptr; - this->size = 0; - - return p; - } - - inline void realloc(size_t newSize) { - this->data = reinterpret_cast(std::realloc(data, newSize)); - if (!this->data) { - throw std::bad_alloc(); - } - - this->size = newSize; - } + void realloc(size_t newSize); OwnedMemoryChunk(const OwnedMemoryChunk&) = delete; OwnedMemoryChunk& operator=(const OwnedMemoryChunk&) = delete; - OwnedMemoryChunk(OwnedMemoryChunk&& other) { - std::tie(this->data, this->size) = other.release(); - } + OwnedMemoryChunk(OwnedMemoryChunk&& other); - OwnedMemoryChunk& operator=(OwnedMemoryChunk&& other) { - if (this != &other) { - std::tie(this->data, this->size) = other.release(); - } + OwnedMemoryChunk& operator=(OwnedMemoryChunk&& other); - return *this; - } + operator bool(); }; }