From 8c42396dc4422bdfd6f955670583077ae6fa6db5 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sun, 21 Sep 2025 16:51:42 +0200 Subject: [PATCH 01/13] Added empty 'ThreadedResourcePrep' sample. --- Samples/Common/src/DefaultSamplesPlugin.cpp | 2 ++ .../include/ThreadedResourcePrep.h | 32 +++++++++++++++++++ .../src/ThreadedResourcePrep.cpp | 22 +++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h create mode 100644 Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp diff --git a/Samples/Common/src/DefaultSamplesPlugin.cpp b/Samples/Common/src/DefaultSamplesPlugin.cpp index 6cb7d84032c..da9e8dc9e74 100644 --- a/Samples/Common/src/DefaultSamplesPlugin.cpp +++ b/Samples/Common/src/DefaultSamplesPlugin.cpp @@ -97,6 +97,7 @@ #endif #include "VolumeTex.h" #include "Water.h" +#include "ThreadedResourcePrep.h" using namespace Ogre; using namespace OgreBites; @@ -176,6 +177,7 @@ DefaultSamplesPlugin::DefaultSamplesPlugin() : SamplePlugin("DefaultSamplesPlugi addSample(new Sample_Terrain); addSample(new Sample_EndlessWorld); #endif + addSample(new Sample_ThreadedResourcePrep); } DefaultSamplesPlugin::~DefaultSamplesPlugin() diff --git a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h new file mode 100644 index 00000000000..799d2ba3308 --- /dev/null +++ b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h @@ -0,0 +1,32 @@ +/* +----------------------------------------------------------------------------- +This source file is part of OGRE + (Object-oriented Graphics Rendering Engine) +For the latest info, see http://www.ogre3d.org/ + +Copyright (c) 2000-2014 Torus Knot Software Ltd +Also see acknowledgements in Readme.html + +You may use this sample code for anything you like, it is not covered by the +same license as the rest of the engine. +----------------------------------------------------------------------------- +*/ + +#ifndef _Sample_ThreadedResourcePrep_H_ +#define _Sample_ThreadedResourcePrep_H_ + +#include "SdkSample.h" +#include "SamplePlugin.h" + +#include + +using namespace Ogre; +using namespace OgreBites; + +class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample +{ +public: + Sample_ThreadedResourcePrep(); +}; + +#endif // _Sample_ThreadedResourcePrep_H_ \ No newline at end of file diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp new file mode 100644 index 00000000000..7b050966177 --- /dev/null +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -0,0 +1,22 @@ +/* +----------------------------------------------------------------------------- +This source file is part of OGRE + (Object-oriented Graphics Rendering Engine) +For the latest info, see http://www.ogre3d.org/ + +Copyright (c) 2000-2014 Torus Knot Software Ltd +Also see acknowledgements in Readme.html + +You may use this sample code for anything you like, it is not covered by the +same license as the rest of the engine. +----------------------------------------------------------------------------- +*/ + +#include "ThreadedResourcePrep.h" + +Sample_ThreadedResourcePrep::Sample_ThreadedResourcePrep() +{ + mInfo["Title"] = "Threaded Resource Prep"; + mInfo["Description"] = "Showcases and benchmarks background resource queue"; + mInfo["Category"] = "Unsorted"; +} From 90c81afe31cc80b6c8fa1f7e3cafb1d4c9755643 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Sun, 21 Sep 2025 21:38:57 +0200 Subject: [PATCH 02/13] ThreadedResourcePrep - base scene and UI --- .../include/ThreadedResourcePrep.h | 36 +++++++++ .../src/ThreadedResourcePrep.cpp | 77 +++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h index 799d2ba3308..541c1b4a401 100644 --- a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h +++ b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h @@ -23,10 +23,46 @@ same license as the rest of the engine. using namespace Ogre; using namespace OgreBites; +struct MeshInfo +{ + // Each resource has dedicated spot, so they can appear in any order. + String meshName; + Vector3 meshScale; + SceneNode* sceneNode = nullptr; + + // Only used when mesh is currently loaded + MeshPtr mesh; + Entity* entity = nullptr; + + // Stats + int totalReloads = 0; + float lastReloadTime = 0.f; + float avgReloadTime = 0.f; + + void recordReloadTime(float reloadTime) + { + totalReloads++; + lastReloadTime = reloadTime; + avgReloadTime = (totalReloads == 1) ? reloadTime : avgReloadTime * 0.5 + reloadTime * 0.5; + } +}; + class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample { public: Sample_ThreadedResourcePrep(); + +private: + // SdkSample setup + void setupContent() override; + void cleanupContent() override; + // Custom setup + void setupSelectableMeshes(); + void defineSelectableMesh(String name, Vector3 pos, Vector3 scale); + void setupControls(); + + std::map mSelectableMeshes; + SelectMenu* mMeshMenu; }; #endif // _Sample_ThreadedResourcePrep_H_ \ No newline at end of file diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp index 7b050966177..618de3165d1 100644 --- a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -20,3 +20,80 @@ Sample_ThreadedResourcePrep::Sample_ThreadedResourcePrep() mInfo["Description"] = "Showcases and benchmarks background resource queue"; mInfo["Category"] = "Unsorted"; } + +void Sample_ThreadedResourcePrep::setupContent() +{ + // Make this viewport work with shader generator scheme. + mViewport->setMaterialScheme(MSN_SHADERGEN); + // update scheme for FFP supporting rendersystems + MaterialManager::getSingleton().setActiveScheme(mViewport->getMaterialScheme()); + + // set background + mViewport->setBackgroundColour(ColourValue(0.9f, 0.9f, 0.7f)); + + // use full ambient lighting + mSceneMgr->setAmbientLight(ColourValue(1.f, 1.f, 1.f)); + + // create a floor mesh resource + MeshManager::getSingleton().createPlane("floor", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + Plane(Vector3::UNIT_Y, 0), 100, 100, 10, 10, true, 1, 10, 10, Vector3::UNIT_Z); + + // create a floor entity, give it a material, and place it at the origin + Entity* floor = mSceneMgr->createEntity("Floor", "floor"); + floor->setMaterialName("Examples/Rockwall"); + floor->setCastShadows(false); + mSceneMgr->getRootSceneNode()->attachObject(floor); + + setupSelectableMeshes(); + setupControls(); + + mCameraMan->setStyle(CS_FREELOOK); +} + +void Sample_ThreadedResourcePrep::cleanupContent() +{ + MeshManager::getSingleton().remove("floor", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); +} + +void Sample_ThreadedResourcePrep::defineSelectableMesh(String name, Vector3 pos, Vector3 scale) +{ + // Start with all meshes in scene + MeshInfo mi; + mi.sceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(pos); + mi.meshName = name; + mi.meshScale = scale; + mi.mesh = MeshManager::getSingleton().load(name, ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + mi.entity = mSceneMgr->createEntity(mi.mesh); + mi.sceneNode->attachObject(mi.entity); + mi.sceneNode->scale(scale); + + mSelectableMeshes[name] = mi; +} + +void Sample_ThreadedResourcePrep::setupSelectableMeshes() +{ + // Each has dedicated spot so they can appear in any order. + // We only want meshes with associated materials - common case. + defineSelectableMesh("ogrehead.mesh",Vector3(0,3,0), Vector3(0.1, 0.1, 0.1)); + defineSelectableMesh("knot.mesh",Vector3(0,5,-10), Vector3(0.04, 0.04, 0.04)); + defineSelectableMesh("spine.mesh",Vector3(-11,0.5,-10), Vector3(0.1, 0.1, 0.1)); + defineSelectableMesh("ninja.mesh",Vector3(-25,0.5,-30), Vector3(0.1, 0.1, 0.1)); + defineSelectableMesh("robot.mesh",Vector3(24,0.5,-32), Vector3(0.1, 0.1, 0.1)); + defineSelectableMesh("Sinbad.mesh",Vector3(30,3,-32), Vector3(1, 1, 1)); + defineSelectableMesh("Sword.mesh",Vector3(33,3,-30), Vector3(1, 1, 1)); + defineSelectableMesh("dragon.mesh",Vector3(0,15,-40), Vector3(0.1, 0.1, 0.1)); +} + +void Sample_ThreadedResourcePrep::setupControls() +{ + mTrayMgr->showCursor(); + + // create a menu to choose the model displayed + mMeshMenu = mTrayMgr->createLongSelectMenu(TL_BOTTOM, "Mesh", "Mesh", 370, 290, 10); + for (const auto& p : mSelectableMeshes) + { + mMeshMenu->addItem(p.first); + } +} + + From 09f67074c1b43a2fe80dc52e5a88abad9ee99a6d Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Mon, 22 Sep 2025 10:56:25 +0200 Subject: [PATCH 03/13] ThreadedResourcePrep sample: fixed camera --- .../include/ThreadedResourcePrep.h | 5 +- .../src/ThreadedResourcePrep.cpp | 52 +++++++++++++------ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h index 541c1b4a401..93884754697 100644 --- a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h +++ b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h @@ -28,6 +28,7 @@ struct MeshInfo // Each resource has dedicated spot, so they can appear in any order. String meshName; Vector3 meshScale; + Degree meshYaw; SceneNode* sceneNode = nullptr; // Only used when mesh is currently loaded @@ -58,8 +59,10 @@ class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample void cleanupContent() override; // Custom setup void setupSelectableMeshes(); - void defineSelectableMesh(String name, Vector3 pos, Vector3 scale); + void defineSelectableMesh(String name, Vector3 pos, Vector3 scale, Degree yaw); void setupControls(); + // SdkSample UI + void itemSelected(SelectMenu* menu) override; std::map mSelectableMeshes; SelectMenu* mMeshMenu; diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp index 618de3165d1..14dc379d02b 100644 --- a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -31,8 +31,11 @@ void Sample_ThreadedResourcePrep::setupContent() // set background mViewport->setBackgroundColour(ColourValue(0.9f, 0.9f, 0.7f)); - // use full ambient lighting - mSceneMgr->setAmbientLight(ColourValue(1.f, 1.f, 1.f)); + // set lights + mSceneMgr->setAmbientLight(ColourValue(0.5, 0.5, 0.4)); + Light* light = mSceneMgr->createLight(); + mSceneMgr->getRootSceneNode()->createChildSceneNode(Vector3(2000, 1000, -1000))->attachObject(light); + light->setDiffuseColour(0.9, 0.9, 0.9); // create a floor mesh resource MeshManager::getSingleton().createPlane("floor", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, @@ -47,7 +50,10 @@ void Sample_ThreadedResourcePrep::setupContent() setupSelectableMeshes(); setupControls(); - mCameraMan->setStyle(CS_FREELOOK); + mCameraMan->setStyle(CS_MANUAL); + SceneNode* camnode = mCameraMan->getCamera(); + camnode->setPosition(Vector3(0,45,80)); + camnode->lookAt(Vector3(0,0,0), Node::TS_WORLD); } void Sample_ThreadedResourcePrep::cleanupContent() @@ -55,33 +61,39 @@ void Sample_ThreadedResourcePrep::cleanupContent() MeshManager::getSingleton().remove("floor", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); } -void Sample_ThreadedResourcePrep::defineSelectableMesh(String name, Vector3 pos, Vector3 scale) +void Sample_ThreadedResourcePrep::defineSelectableMesh(String name, Vector3 pos, Vector3 scale, Degree yaw) { - // Start with all meshes in scene + // Fill mesh info MeshInfo mi; mi.sceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(pos); mi.meshName = name; mi.meshScale = scale; + mi.meshYaw = yaw; + + // Start with all meshes in scene mi.mesh = MeshManager::getSingleton().load(name, ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); mi.entity = mSceneMgr->createEntity(mi.mesh); mi.sceneNode->attachObject(mi.entity); mi.sceneNode->scale(scale); + mi.sceneNode->yaw(yaw); mSelectableMeshes[name] = mi; } void Sample_ThreadedResourcePrep::setupSelectableMeshes() { - // Each has dedicated spot so they can appear in any order. - // We only want meshes with associated materials - common case. - defineSelectableMesh("ogrehead.mesh",Vector3(0,3,0), Vector3(0.1, 0.1, 0.1)); - defineSelectableMesh("knot.mesh",Vector3(0,5,-10), Vector3(0.04, 0.04, 0.04)); - defineSelectableMesh("spine.mesh",Vector3(-11,0.5,-10), Vector3(0.1, 0.1, 0.1)); - defineSelectableMesh("ninja.mesh",Vector3(-25,0.5,-30), Vector3(0.1, 0.1, 0.1)); - defineSelectableMesh("robot.mesh",Vector3(24,0.5,-32), Vector3(0.1, 0.1, 0.1)); - defineSelectableMesh("Sinbad.mesh",Vector3(30,3,-32), Vector3(1, 1, 1)); - defineSelectableMesh("Sword.mesh",Vector3(33,3,-30), Vector3(1, 1, 1)); - defineSelectableMesh("dragon.mesh",Vector3(0,15,-40), Vector3(0.1, 0.1, 0.1)); + // Each mesh has dedicated spot so they can appear in any order. + // We only want meshes with associated materials (the common case). + + defineSelectableMesh("ogrehead.mesh",Vector3(0,3,0), Vector3(0.3, 0.3, 0.3), Degree(0)); + defineSelectableMesh("penguin.mesh",Vector3(10,3,0), Vector3(0.1, 0.1, 0.1), Degree(0)); + defineSelectableMesh("knot.mesh",Vector3(0,5,-10), Vector3(0.04, 0.04, 0.04), Degree(0)); + defineSelectableMesh("spine.mesh",Vector3(-11,0.5,-10), Vector3(0.1, 0.1, 0.1), Degree(0)); + defineSelectableMesh("ninja.mesh",Vector3(-25,0.5,5), Vector3(0.05, 0.05, 0.05), Degree(180)); + defineSelectableMesh("robot.mesh",Vector3(24,0.5,-32), Vector3(0.1, 0.1, 0.1), Degree(-90)); + defineSelectableMesh("Sinbad.mesh",Vector3(30,5,5), Vector3(1, 1, 1), Degree(-25)); + defineSelectableMesh("Sword.mesh",Vector3(33,3,10), Vector3(1, 1, 1), Degree(80)); + defineSelectableMesh("dragon.mesh",Vector3(-15,25,-25), Vector3(0.1, 0.1, 0.1), Degree(200)); } void Sample_ThreadedResourcePrep::setupControls() @@ -96,4 +108,14 @@ void Sample_ThreadedResourcePrep::setupControls() } } +void Sample_ThreadedResourcePrep::itemSelected(SelectMenu* menu) +{ + if (menu == mMeshMenu) + { + // focus camera + SceneNode* camnode = mCameraMan->getCamera(); + camnode->lookAt(mSelectableMeshes[mMeshMenu->getSelectedItem()].sceneNode->getPosition(), Node::TS_WORLD); + } +} + From 1872d0d89b43613dbf3250899cb2f5a8c4d249c7 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Mon, 22 Sep 2025 20:21:25 +0200 Subject: [PATCH 04/13] ThreadedResourcePrep sample: initial sync reloading + stat --- .../include/ThreadedResourcePrep.h | 9 ++++ .../src/ThreadedResourcePrep.cpp | 48 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h index 93884754697..00609897470 100644 --- a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h +++ b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h @@ -63,9 +63,18 @@ class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample void setupControls(); // SdkSample UI void itemSelected(SelectMenu* menu) override; + void buttonHit(Button* button) override; + // Custom UI + void refreshMeshStat(); + + // The heavy lifting + void performReload(); std::map mSelectableMeshes; SelectMenu* mMeshMenu; + Label* mMeshStatLabel; + Button * mReloadBtn; + Timer mTimer; }; #endif // _Sample_ThreadedResourcePrep_H_ \ No newline at end of file diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp index 14dc379d02b..cf702595552 100644 --- a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -14,6 +14,8 @@ same license as the rest of the engine. #include "ThreadedResourcePrep.h" +#include + Sample_ThreadedResourcePrep::Sample_ThreadedResourcePrep() { mInfo["Title"] = "Threaded Resource Prep"; @@ -100,12 +102,20 @@ void Sample_ThreadedResourcePrep::setupControls() { mTrayMgr->showCursor(); + const float BOTTOM_W = 370; + // create a menu to choose the model displayed - mMeshMenu = mTrayMgr->createLongSelectMenu(TL_BOTTOM, "Mesh", "Mesh", 370, 290, 10); + mMeshMenu = mTrayMgr->createLongSelectMenu(TL_BOTTOM, "Mesh", "Mesh", BOTTOM_W, 290, 10); for (const auto& p : mSelectableMeshes) { mMeshMenu->addItem(p.first); } + mMeshMenu->selectItem(0); + + mMeshStatLabel = mTrayMgr->createLabel(TL_BOTTOM, "MeshStat", "", BOTTOM_W); + refreshMeshStat(); + + mReloadBtn = mTrayMgr->createButton(TL_BOTTOM, "ReloadBtn", "Reload!", BOTTOM_W/2); } void Sample_ThreadedResourcePrep::itemSelected(SelectMenu* menu) @@ -115,7 +125,43 @@ void Sample_ThreadedResourcePrep::itemSelected(SelectMenu* menu) // focus camera SceneNode* camnode = mCameraMan->getCamera(); camnode->lookAt(mSelectableMeshes[mMeshMenu->getSelectedItem()].sceneNode->getPosition(), Node::TS_WORLD); + + refreshMeshStat(); } } +void Sample_ThreadedResourcePrep::refreshMeshStat() +{ + MeshInfo& mi = mSelectableMeshes[mMeshMenu->getSelectedItem()]; + std::stringstream buf; + buf << "Reloads: " << mi.totalReloads << ", Avg. time: " << mi.avgReloadTime << " (Last: " << mi.lastReloadTime << ")"; + mMeshStatLabel->setCaption(buf.str()); +} + +void Sample_ThreadedResourcePrep::performReload() +{ + MeshInfo& mi = mSelectableMeshes[mMeshMenu->getSelectedItem()]; + + // Sync unload + mi.sceneNode->destroyAllObjects(); + mi.entity = nullptr; + mi.mesh->unload(); + // Sync load + long long start = mTimer.getMilliseconds(); + mi.mesh->load(); + mi.entity = mSceneMgr->createEntity(mi.mesh); + mi.sceneNode->attachObject(mi.entity); + long long end = mTimer.getMilliseconds(); + + mi.recordReloadTime(static_cast(end - start) * 0.001); +} + +void Sample_ThreadedResourcePrep::buttonHit(Button* button) +{ + if (button == mReloadBtn) + { + performReload(); + refreshMeshStat(); + } +} From 2b8372d61cc9dc6304a47822cc6ac1594792af37 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Tue, 23 Sep 2025 20:57:26 +0200 Subject: [PATCH 05/13] ThreadedResourcePrep sample: bulk size slider --- .../include/ThreadedResourcePrep.h | 25 ++- .../src/ThreadedResourcePrep.cpp | 162 ++++++++++++++---- 2 files changed, 151 insertions(+), 36 deletions(-) diff --git a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h index 00609897470..9f3632c40d1 100644 --- a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h +++ b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h @@ -19,6 +19,7 @@ same license as the rest of the engine. #include "SamplePlugin.h" #include +#include using namespace Ogre; using namespace OgreBites; @@ -34,8 +35,10 @@ struct MeshInfo // Only used when mesh is currently loaded MeshPtr mesh; Entity* entity = nullptr; +}; - // Stats +struct Stats +{ int totalReloads = 0; float lastReloadTime = 0.f; float avgReloadTime = 0.f; @@ -46,6 +49,13 @@ struct MeshInfo lastReloadTime = reloadTime; avgReloadTime = (totalReloads == 1) ? reloadTime : avgReloadTime * 0.5 + reloadTime * 0.5; } + + void resetStats() + { + totalReloads = 0; + lastReloadTime = 0; + avgReloadTime = 0; + } }; class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample @@ -64,17 +74,24 @@ class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample // SdkSample UI void itemSelected(SelectMenu* menu) override; void buttonHit(Button* button) override; + void sliderMoved(Slider* slider) override; // Custom UI - void refreshMeshStat(); + void refreshStatsUi(); + void refreshMeshUi(); // The heavy lifting void performReload(); + static void forceUnloadAllDependentResources(MeshPtr& mesh); + static void forceUnloadAllDependentTextures(Pass* pass); - std::map mSelectableMeshes; + std::deque mMeshQueue; SelectMenu* mMeshMenu; Label* mMeshStatLabel; - Button * mReloadBtn; + Button* mReloadBtn; + Slider* mBulkSlider; + size_t mBulkSize = 0; Timer mTimer; + Stats mStats; }; #endif // _Sample_ThreadedResourcePrep_H_ \ No newline at end of file diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp index cf702595552..79cb4ccc020 100644 --- a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -15,6 +15,7 @@ same license as the rest of the engine. #include "ThreadedResourcePrep.h" #include +#include Sample_ThreadedResourcePrep::Sample_ThreadedResourcePrep() { @@ -36,8 +37,8 @@ void Sample_ThreadedResourcePrep::setupContent() // set lights mSceneMgr->setAmbientLight(ColourValue(0.5, 0.5, 0.4)); Light* light = mSceneMgr->createLight(); - mSceneMgr->getRootSceneNode()->createChildSceneNode(Vector3(2000, 1000, -1000))->attachObject(light); - light->setDiffuseColour(0.9, 0.9, 0.9); + mSceneMgr->getRootSceneNode()->createChildSceneNode(Vector3(200, 100, 100))->attachObject(light); + light->setDiffuseColour(0.9, 0.9, 0.95); // create a floor mesh resource MeshManager::getSingleton().createPlane("floor", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, @@ -79,7 +80,7 @@ void Sample_ThreadedResourcePrep::defineSelectableMesh(String name, Vector3 pos, mi.sceneNode->scale(scale); mi.sceneNode->yaw(yaw); - mSelectableMeshes[name] = mi; + mMeshQueue.push_back(mi); } void Sample_ThreadedResourcePrep::setupSelectableMeshes() @@ -87,15 +88,24 @@ void Sample_ThreadedResourcePrep::setupSelectableMeshes() // Each mesh has dedicated spot so they can appear in any order. // We only want meshes with associated materials (the common case). - defineSelectableMesh("ogrehead.mesh",Vector3(0,3,0), Vector3(0.3, 0.3, 0.3), Degree(0)); - defineSelectableMesh("penguin.mesh",Vector3(10,3,0), Vector3(0.1, 0.1, 0.1), Degree(0)); - defineSelectableMesh("knot.mesh",Vector3(0,5,-10), Vector3(0.04, 0.04, 0.04), Degree(0)); - defineSelectableMesh("spine.mesh",Vector3(-11,0.5,-10), Vector3(0.1, 0.1, 0.1), Degree(0)); - defineSelectableMesh("ninja.mesh",Vector3(-25,0.5,5), Vector3(0.05, 0.05, 0.05), Degree(180)); - defineSelectableMesh("robot.mesh",Vector3(24,0.5,-32), Vector3(0.1, 0.1, 0.1), Degree(-90)); + defineSelectableMesh("ogrehead.mesh",Vector3(0,3,0), Vector3(0.2, 0.2, 0.2), Degree(0)); + defineSelectableMesh("facial.mesh",Vector3(10,3,-15), Vector3(0.1, 0.1, 0.1), Degree(0)); + defineSelectableMesh("penguin.mesh",Vector3(12,3,0), Vector3(0.1, 0.1, 0.1), Degree(0)); + defineSelectableMesh("knot.mesh",Vector3(-14,5,16), Vector3(0.04, 0.04, 0.04), Degree(0)); + defineSelectableMesh("spine.mesh",Vector3(-11,0.5,0), Vector3(0.1, 0.1, 0.1), Degree(0)); + defineSelectableMesh("ninja.mesh",Vector3(-25,0.5,5), Vector3(0.05, 0.05, 0.05), Degree(205)); + defineSelectableMesh("jaiqua.mesh",Vector3(-20,0.5,-5), Vector3(0.5, 0.5, 0.5), Degree(180)); + defineSelectableMesh("fish.mesh",Vector3(20,0.5,-5), Vector3(1, 1, 1), Degree(0)); + defineSelectableMesh("robot.mesh",Vector3(-20,0.5,-2), Vector3(0.1, 0.1, 0.1), Degree(-85)); defineSelectableMesh("Sinbad.mesh",Vector3(30,5,5), Vector3(1, 1, 1), Degree(-25)); - defineSelectableMesh("Sword.mesh",Vector3(33,3,10), Vector3(1, 1, 1), Degree(80)); - defineSelectableMesh("dragon.mesh",Vector3(-15,25,-25), Vector3(0.1, 0.1, 0.1), Degree(200)); + defineSelectableMesh("Sword.mesh",Vector3(11,5,18), Vector3(1, 1, 1), Degree(75)); + defineSelectableMesh("Barrel.mesh",Vector3(12,2,18), Vector3(1, 1, 1), Degree(0)); + defineSelectableMesh("DamagedHelmet.mesh",Vector3(22,1.5,18), Vector3(4, 4, 4), Degree(0)); + defineSelectableMesh("dragon.mesh",Vector3(-5,30,10), Vector3(0.1, 0.1, 0.1), Degree(216)); + defineSelectableMesh("razor.mesh",Vector3(15,25,10), Vector3(0.1, 0.1, 0.1), Degree(-16)); + defineSelectableMesh("sibenik.mesh",Vector3(0,0,-35), Vector3(0.5, 0.5, 0.5), Degree(180)); + defineSelectableMesh("geosphere4500.mesh",Vector3(40,0,-25), Vector3(0.01, 0.01, 0.01), Degree(180)); + defineSelectableMesh("geosphere8000.mesh",Vector3(-40,0,-25), Vector3(0.01, 0.01, 0.01), Degree(180)); } void Sample_ThreadedResourcePrep::setupControls() @@ -106,55 +116,110 @@ void Sample_ThreadedResourcePrep::setupControls() // create a menu to choose the model displayed mMeshMenu = mTrayMgr->createLongSelectMenu(TL_BOTTOM, "Mesh", "Mesh", BOTTOM_W, 290, 10); - for (const auto& p : mSelectableMeshes) - { - mMeshMenu->addItem(p.first); - } - mMeshMenu->selectItem(0); mMeshStatLabel = mTrayMgr->createLabel(TL_BOTTOM, "MeshStat", "", BOTTOM_W); - refreshMeshStat(); + refreshStatsUi(); mReloadBtn = mTrayMgr->createButton(TL_BOTTOM, "ReloadBtn", "Reload!", BOTTOM_W/2); + + mBulkSlider = mTrayMgr->createThickSlider(TL_TOPLEFT, "BulkSlider", "Meshes in bulk", 200, 50, 1, (Real)mMeshQueue.size(), (int)mMeshQueue.size()); + + refreshMeshUi(); // Initial fill of mesh menu + mMeshMenu->selectItem(0); // Initial selection (CAUTION: invokes callbacks!) } void Sample_ThreadedResourcePrep::itemSelected(SelectMenu* menu) { if (menu == mMeshMenu) { + // Find selected mesh by name (account for "" in name) + StringVector toks = Ogre::StringUtil::split(mMeshMenu->getSelectedItem()); + auto selectedMeshItor = std::find_if (mMeshQueue.begin(), mMeshQueue.end(), + [&toks](MeshInfo& mi) + { + return mi.meshName == toks[0]; + }); + // focus camera SceneNode* camnode = mCameraMan->getCamera(); - camnode->lookAt(mSelectableMeshes[mMeshMenu->getSelectedItem()].sceneNode->getPosition(), Node::TS_WORLD); + camnode->lookAt(selectedMeshItor->sceneNode->getPosition(), Node::TS_WORLD); + + // Move selected item to front of queue + MeshInfo mi = *selectedMeshItor; + mMeshQueue.erase(selectedMeshItor); + mMeshQueue.push_front(mi); + refreshMeshUi(); + + mStats.resetStats(); + refreshStatsUi(); + } +} - refreshMeshStat(); +void Sample_ThreadedResourcePrep::sliderMoved(Slider* slider) +{ + if (slider == mBulkSlider) + { + size_t bulkSize = (size_t)mBulkSlider->getValue(); + if (bulkSize != mBulkSize) + { + mBulkSize = bulkSize; + mReloadBtn->setCaption("Reload " + StringConverter::toString((int)mBulkSlider->getValue()) + " mesh(es)"); + refreshMeshUi(); + } } } -void Sample_ThreadedResourcePrep::refreshMeshStat() +void Sample_ThreadedResourcePrep::refreshStatsUi() { - MeshInfo& mi = mSelectableMeshes[mMeshMenu->getSelectedItem()]; std::stringstream buf; - buf << "Reloads: " << mi.totalReloads << ", Avg. time: " << mi.avgReloadTime << " (Last: " << mi.lastReloadTime << ")"; + buf << "Reloads: " << mStats.totalReloads + << ", Avg. time: " << std::setprecision(3) << mStats.avgReloadTime + << " (Last: " << std::setprecision(3) << mStats.lastReloadTime << ")"; mMeshStatLabel->setCaption(buf.str()); } +void Sample_ThreadedResourcePrep::refreshMeshUi() +{ + mMeshMenu->clearItems(); + + for (size_t i=0; iaddItem(mMeshQueue[i].meshName + " "); + } + else + { + mMeshMenu->addItem(mMeshQueue[i].meshName); + } + } +} + void Sample_ThreadedResourcePrep::performReload() { - MeshInfo& mi = mSelectableMeshes[mMeshMenu->getSelectedItem()]; + for (size_t i = 0; i < mBulkSize; i++) + { + MeshInfo& mi = mMeshQueue[i]; - // Sync unload - mi.sceneNode->destroyAllObjects(); - mi.entity = nullptr; - mi.mesh->unload(); + // Sync unload + mi.sceneNode->destroyAllObjects(); + mi.entity = nullptr; + forceUnloadAllDependentResources(mi.mesh); + mi.mesh->unload(); + } // Sync load long long start = mTimer.getMilliseconds(); - mi.mesh->load(); - mi.entity = mSceneMgr->createEntity(mi.mesh); - mi.sceneNode->attachObject(mi.entity); + for (size_t i = 0; i < mBulkSize; i++) + { + MeshInfo& mi = mMeshQueue[i]; + mi.mesh->load(); + mi.entity = mSceneMgr->createEntity(mi.mesh); + mi.sceneNode->attachObject(mi.entity); + } long long end = mTimer.getMilliseconds(); - mi.recordReloadTime(static_cast(end - start) * 0.001); + mStats.recordReloadTime(static_cast(end - start) * 0.001); } void Sample_ThreadedResourcePrep::buttonHit(Button* button) @@ -162,6 +227,39 @@ void Sample_ThreadedResourcePrep::buttonHit(Button* button) if (button == mReloadBtn) { performReload(); - refreshMeshStat(); + refreshStatsUi(); + } +} + +void Sample_ThreadedResourcePrep::forceUnloadAllDependentTextures(Pass* pass) +{ + for (TextureUnitState* tus: pass->getTextureUnitStates()) + { + for (size_t iFrame = 0; iFrame < tus->getNumFrames(); iFrame++) + { + TexturePtr tex = Ogre::TextureManager::getSingleton().getByName(tus->getFrameTextureName(iFrame)); + if (tex) + { + tex->unload(); + } + } + } +} + +void Sample_ThreadedResourcePrep::forceUnloadAllDependentResources(MeshPtr& mesh) +{ + for (SubMesh* submesh: mesh->getSubMeshes()) + { + if (submesh->getMaterial()) + { + for (Technique* teq: submesh->getMaterial()->getSupportedTechniques()) + { + for (Pass* pass: teq->getPasses()) + { + forceUnloadAllDependentTextures(pass); + } + } + submesh->getMaterial()->unload(); + } } } From ba8cb51f0dcc23168170783aca3ceb38ed1ecfed Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 25 Sep 2025 01:05:44 +0200 Subject: [PATCH 06/13] ThreadResourcePrep: [Unload batch] btn. --- .../include/ThreadedResourcePrep.h | 8 ++- .../src/ThreadedResourcePrep.cpp | 62 ++++++++++++------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h index 9f3632c40d1..335560ad926 100644 --- a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h +++ b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h @@ -80,7 +80,8 @@ class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample void refreshMeshUi(); // The heavy lifting - void performReload(); + void performSyncUnload(); + void performSyncLoad(); static void forceUnloadAllDependentResources(MeshPtr& mesh); static void forceUnloadAllDependentTextures(Pass* pass); @@ -88,8 +89,9 @@ class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample SelectMenu* mMeshMenu; Label* mMeshStatLabel; Button* mReloadBtn; - Slider* mBulkSlider; - size_t mBulkSize = 0; + Button* mUnloadBtn; + Slider* mBatchSlider; + size_t mBatchSize = 0; Timer mTimer; Stats mStats; }; diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp index 79cb4ccc020..201b7a55bd0 100644 --- a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -64,11 +64,13 @@ void Sample_ThreadedResourcePrep::cleanupContent() MeshManager::getSingleton().remove("floor", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); } +const Vector3 TIGHTEN_SPOTS(0.7, 1, 0.7); + void Sample_ThreadedResourcePrep::defineSelectableMesh(String name, Vector3 pos, Vector3 scale, Degree yaw) { // Fill mesh info MeshInfo mi; - mi.sceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(pos); + mi.sceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(pos * TIGHTEN_SPOTS); mi.meshName = name; mi.meshScale = scale; mi.meshYaw = yaw; @@ -86,16 +88,17 @@ void Sample_ThreadedResourcePrep::defineSelectableMesh(String name, Vector3 pos, void Sample_ThreadedResourcePrep::setupSelectableMeshes() { // Each mesh has dedicated spot so they can appear in any order. - // We only want meshes with associated materials (the common case). + // We prefer meshes with associated materials (the common case). + defineSelectableMesh("sibenik.mesh",Vector3(0,0,-35), Vector3(0.5, 0.5, 0.5), Degree(180)); defineSelectableMesh("ogrehead.mesh",Vector3(0,3,0), Vector3(0.2, 0.2, 0.2), Degree(0)); - defineSelectableMesh("facial.mesh",Vector3(10,3,-15), Vector3(0.1, 0.1, 0.1), Degree(0)); + defineSelectableMesh("facial.mesh",Vector3(-35,3,-1), Vector3(0.1, 0.1, 0.1), Degree(0)); defineSelectableMesh("penguin.mesh",Vector3(12,3,0), Vector3(0.1, 0.1, 0.1), Degree(0)); defineSelectableMesh("knot.mesh",Vector3(-14,5,16), Vector3(0.04, 0.04, 0.04), Degree(0)); defineSelectableMesh("spine.mesh",Vector3(-11,0.5,0), Vector3(0.1, 0.1, 0.1), Degree(0)); defineSelectableMesh("ninja.mesh",Vector3(-25,0.5,5), Vector3(0.05, 0.05, 0.05), Degree(205)); - defineSelectableMesh("jaiqua.mesh",Vector3(-20,0.5,-5), Vector3(0.5, 0.5, 0.5), Degree(180)); - defineSelectableMesh("fish.mesh",Vector3(20,0.5,-5), Vector3(1, 1, 1), Degree(0)); + defineSelectableMesh("jaiqua.mesh",Vector3(-23,0.5,-9), Vector3(0.5, 0.5, 0.5), Degree(180)); + defineSelectableMesh("fish.mesh",Vector3(-2,0.5,20), Vector3(1, 1, 1), Degree(0)); defineSelectableMesh("robot.mesh",Vector3(-20,0.5,-2), Vector3(0.1, 0.1, 0.1), Degree(-85)); defineSelectableMesh("Sinbad.mesh",Vector3(30,5,5), Vector3(1, 1, 1), Degree(-25)); defineSelectableMesh("Sword.mesh",Vector3(11,5,18), Vector3(1, 1, 1), Degree(75)); @@ -103,7 +106,7 @@ void Sample_ThreadedResourcePrep::setupSelectableMeshes() defineSelectableMesh("DamagedHelmet.mesh",Vector3(22,1.5,18), Vector3(4, 4, 4), Degree(0)); defineSelectableMesh("dragon.mesh",Vector3(-5,30,10), Vector3(0.1, 0.1, 0.1), Degree(216)); defineSelectableMesh("razor.mesh",Vector3(15,25,10), Vector3(0.1, 0.1, 0.1), Degree(-16)); - defineSelectableMesh("sibenik.mesh",Vector3(0,0,-35), Vector3(0.5, 0.5, 0.5), Degree(180)); + // these have no material but are big - good for the benchmark defineSelectableMesh("geosphere4500.mesh",Vector3(40,0,-25), Vector3(0.01, 0.01, 0.01), Degree(180)); defineSelectableMesh("geosphere8000.mesh",Vector3(-40,0,-25), Vector3(0.01, 0.01, 0.01), Degree(180)); } @@ -113,6 +116,7 @@ void Sample_ThreadedResourcePrep::setupControls() mTrayMgr->showCursor(); const float BOTTOM_W = 370; + const float TOPLEFT_W = 200; // create a menu to choose the model displayed mMeshMenu = mTrayMgr->createLongSelectMenu(TL_BOTTOM, "Mesh", "Mesh", BOTTOM_W, 290, 10); @@ -122,10 +126,15 @@ void Sample_ThreadedResourcePrep::setupControls() mReloadBtn = mTrayMgr->createButton(TL_BOTTOM, "ReloadBtn", "Reload!", BOTTOM_W/2); - mBulkSlider = mTrayMgr->createThickSlider(TL_TOPLEFT, "BulkSlider", "Meshes in bulk", 200, 50, 1, (Real)mMeshQueue.size(), (int)mMeshQueue.size()); + mBatchSlider = mTrayMgr->createThickSlider(TL_TOPLEFT, "BatchSlider", "Batch length", TOPLEFT_W, 50, 1, (Real)mMeshQueue.size(), (int)mMeshQueue.size()); + + mUnloadBtn = mTrayMgr->createButton(TL_TOPLEFT, "UnloadBtn", "Unload batch", TOPLEFT_W); + + mBatchSlider->setValue((Real)mMeshQueue.size(), /* notifyListener: */false); + mBatchSize = mMeshQueue.size(); - refreshMeshUi(); // Initial fill of mesh menu - mMeshMenu->selectItem(0); // Initial selection (CAUTION: invokes callbacks!) + refreshMeshUi(); + mMeshMenu->selectItem(0, /* notifyListener: */false); } void Sample_ThreadedResourcePrep::itemSelected(SelectMenu* menu) @@ -157,14 +166,15 @@ void Sample_ThreadedResourcePrep::itemSelected(SelectMenu* menu) void Sample_ThreadedResourcePrep::sliderMoved(Slider* slider) { - if (slider == mBulkSlider) + if (slider == mBatchSlider) { - size_t bulkSize = (size_t)mBulkSlider->getValue(); - if (bulkSize != mBulkSize) + size_t bulkSize = (size_t)mBatchSlider->getValue(); + if (bulkSize != mBatchSize) { - mBulkSize = bulkSize; - mReloadBtn->setCaption("Reload " + StringConverter::toString((int)mBulkSlider->getValue()) + " mesh(es)"); + mBatchSize = bulkSize; refreshMeshUi(); + mStats.resetStats(); + refreshStatsUi(); } } } @@ -184,33 +194,38 @@ void Sample_ThreadedResourcePrep::refreshMeshUi() for (size_t i=0; iaddItem(mMeshQueue[i].meshName + " "); + mMeshMenu->addItem(mMeshQueue[i].meshName + " (in batch)"); } else { mMeshMenu->addItem(mMeshQueue[i].meshName); } } + + mReloadBtn->setCaption("Reload " + StringConverter::toString((int)mBatchSlider->getValue()) + " mesh(es)"); } -void Sample_ThreadedResourcePrep::performReload() +void Sample_ThreadedResourcePrep::performSyncUnload() { - for (size_t i = 0; i < mBulkSize; i++) + // Sync unload + for (size_t i = 0; i < mBatchSize; i++) { MeshInfo& mi = mMeshQueue[i]; - // Sync unload mi.sceneNode->destroyAllObjects(); mi.entity = nullptr; forceUnloadAllDependentResources(mi.mesh); mi.mesh->unload(); } +} +void Sample_ThreadedResourcePrep::performSyncLoad() +{ // Sync load long long start = mTimer.getMilliseconds(); - for (size_t i = 0; i < mBulkSize; i++) + for (size_t i = 0; i < mBatchSize; i++) { MeshInfo& mi = mMeshQueue[i]; mi.mesh->load(); @@ -226,9 +241,14 @@ void Sample_ThreadedResourcePrep::buttonHit(Button* button) { if (button == mReloadBtn) { - performReload(); + performSyncUnload(); + performSyncLoad(); refreshStatsUi(); } + else if (button == mUnloadBtn) + { + performSyncUnload(); + } } void Sample_ThreadedResourcePrep::forceUnloadAllDependentTextures(Pass* pass) From 1b4aa6dd0a4935af871ddee90b5551285b99528e Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 25 Sep 2025 02:41:15 +0200 Subject: [PATCH 07/13] ThreadedResourcePrep: threaded prep! --- .../include/ThreadedResourcePrep.h | 17 ++- .../src/ThreadedResourcePrep.cpp | 109 +++++++++++++++--- 2 files changed, 104 insertions(+), 22 deletions(-) diff --git a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h index 335560ad926..eb43e81fb23 100644 --- a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h +++ b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h @@ -58,7 +58,7 @@ struct Stats } }; -class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample +class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample, public Resource::Listener { public: Sample_ThreadedResourcePrep(); @@ -75,25 +75,36 @@ class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample void itemSelected(SelectMenu* menu) override; void buttonHit(Button* button) override; void sliderMoved(Slider* slider) override; + void checkBoxToggled(CheckBox* box) override; // Custom UI void refreshStatsUi(); void refreshMeshUi(); // The heavy lifting void performSyncUnload(); - void performSyncLoad(); + void performSyncPrep(); + void loadMeshOnQueue(size_t i); + void performThreadedPrep(); static void forceUnloadAllDependentResources(MeshPtr& mesh); static void forceUnloadAllDependentTextures(Pass* pass); + // Resource listener + void preparingComplete(Resource*) override; + std::deque mMeshQueue; SelectMenu* mMeshMenu; Label* mMeshStatLabel; + Stats mStats; Button* mReloadBtn; Button* mUnloadBtn; Slider* mBatchSlider; size_t mBatchSize = 0; Timer mTimer; - Stats mStats; + CheckBox* mThreadedMeshChk; + + // accumulators of `loadMeshOnQueue()`: + long long mLoadingTotalMillis; + size_t mNumLoadedMeshes; }; #endif // _Sample_ThreadedResourcePrep_H_ \ No newline at end of file diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp index 201b7a55bd0..7009376a070 100644 --- a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -55,13 +55,19 @@ void Sample_ThreadedResourcePrep::setupContent() mCameraMan->setStyle(CS_MANUAL); SceneNode* camnode = mCameraMan->getCamera(); - camnode->setPosition(Vector3(0,45,80)); - camnode->lookAt(Vector3(0,0,0), Node::TS_WORLD); + camnode->setPosition(Vector3(0,20,60)); + camnode->lookAt(Vector3(0,0,-45), Node::TS_WORLD); } void Sample_ThreadedResourcePrep::cleanupContent() { MeshManager::getSingleton().remove("floor", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + + for (size_t i=0; iremoveListener(this); + } } const Vector3 TIGHTEN_SPOTS(0.7, 1, 0.7); @@ -77,6 +83,7 @@ void Sample_ThreadedResourcePrep::defineSelectableMesh(String name, Vector3 pos, // Start with all meshes in scene mi.mesh = MeshManager::getSingleton().load(name, ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + mi.mesh->addListener(this); mi.entity = mSceneMgr->createEntity(mi.mesh); mi.sceneNode->attachObject(mi.entity); mi.sceneNode->scale(scale); @@ -91,21 +98,21 @@ void Sample_ThreadedResourcePrep::setupSelectableMeshes() // We prefer meshes with associated materials (the common case). defineSelectableMesh("sibenik.mesh",Vector3(0,0,-35), Vector3(0.5, 0.5, 0.5), Degree(180)); - defineSelectableMesh("ogrehead.mesh",Vector3(0,3,0), Vector3(0.2, 0.2, 0.2), Degree(0)); - defineSelectableMesh("facial.mesh",Vector3(-35,3,-1), Vector3(0.1, 0.1, 0.1), Degree(0)); + defineSelectableMesh("ogrehead.mesh",Vector3(0,3.5,0), Vector3(0.2, 0.2, 0.2), Degree(0)); + defineSelectableMesh("facial.mesh",Vector3(-35,3,2), Vector3(0.1, 0.1, 0.1), Degree(0)); defineSelectableMesh("penguin.mesh",Vector3(12,3,0), Vector3(0.1, 0.1, 0.1), Degree(0)); defineSelectableMesh("knot.mesh",Vector3(-14,5,16), Vector3(0.04, 0.04, 0.04), Degree(0)); defineSelectableMesh("spine.mesh",Vector3(-11,0.5,0), Vector3(0.1, 0.1, 0.1), Degree(0)); defineSelectableMesh("ninja.mesh",Vector3(-25,0.5,5), Vector3(0.05, 0.05, 0.05), Degree(205)); - defineSelectableMesh("jaiqua.mesh",Vector3(-23,0.5,-9), Vector3(0.5, 0.5, 0.5), Degree(180)); + defineSelectableMesh("jaiqua.mesh",Vector3(-23,0.5,-12), Vector3(0.5, 0.5, 0.5), Degree(180)); defineSelectableMesh("fish.mesh",Vector3(-2,0.5,20), Vector3(1, 1, 1), Degree(0)); defineSelectableMesh("robot.mesh",Vector3(-20,0.5,-2), Vector3(0.1, 0.1, 0.1), Degree(-85)); defineSelectableMesh("Sinbad.mesh",Vector3(30,5,5), Vector3(1, 1, 1), Degree(-25)); - defineSelectableMesh("Sword.mesh",Vector3(11,5,18), Vector3(1, 1, 1), Degree(75)); - defineSelectableMesh("Barrel.mesh",Vector3(12,2,18), Vector3(1, 1, 1), Degree(0)); - defineSelectableMesh("DamagedHelmet.mesh",Vector3(22,1.5,18), Vector3(4, 4, 4), Degree(0)); - defineSelectableMesh("dragon.mesh",Vector3(-5,30,10), Vector3(0.1, 0.1, 0.1), Degree(216)); - defineSelectableMesh("razor.mesh",Vector3(15,25,10), Vector3(0.1, 0.1, 0.1), Degree(-16)); + defineSelectableMesh("Sword.mesh",Vector3(10.5,5,18), Vector3(1, 1, 1), Degree(75)); + defineSelectableMesh("Barrel.mesh",Vector3(12,3,18), Vector3(1, 1, 1), Degree(0)); + defineSelectableMesh("DamagedHelmet.mesh",Vector3(22,2.5,18), Vector3(4, 4, 4), Degree(0)); + defineSelectableMesh("dragon.mesh",Vector3(75,28,-25), Vector3(0.2, 0.2, 0.2), Degree(203)); + defineSelectableMesh("razor.mesh",Vector3(-15,17,10), Vector3(0.1, 0.1, 0.1), Degree(-16)); // these have no material but are big - good for the benchmark defineSelectableMesh("geosphere4500.mesh",Vector3(40,0,-25), Vector3(0.01, 0.01, 0.01), Degree(180)); defineSelectableMesh("geosphere8000.mesh",Vector3(-40,0,-25), Vector3(0.01, 0.01, 0.01), Degree(180)); @@ -128,6 +135,8 @@ void Sample_ThreadedResourcePrep::setupControls() mBatchSlider = mTrayMgr->createThickSlider(TL_TOPLEFT, "BatchSlider", "Batch length", TOPLEFT_W, 50, 1, (Real)mMeshQueue.size(), (int)mMeshQueue.size()); + mThreadedMeshChk = mTrayMgr->createCheckBox(TL_TOPLEFT, "ThrMeshChk", "Bg. mesh prep", TOPLEFT_W); + mUnloadBtn = mTrayMgr->createButton(TL_TOPLEFT, "UnloadBtn", "Unload batch", TOPLEFT_W); mBatchSlider->setValue((Real)mMeshQueue.size(), /* notifyListener: */false); @@ -221,20 +230,64 @@ void Sample_ThreadedResourcePrep::performSyncUnload() } } -void Sample_ThreadedResourcePrep::performSyncLoad() +void Sample_ThreadedResourcePrep::performSyncPrep() +{ + mNumLoadedMeshes = 0; + for (size_t i = 0; i < mBatchSize; i++) + { + loadMeshOnQueue(i); + } +} + +void Sample_ThreadedResourcePrep::loadMeshOnQueue(size_t i) { - // Sync load - long long start = mTimer.getMilliseconds(); + long long startMillis = mTimer.getMilliseconds(); + + MeshInfo& mi = mMeshQueue[i]; + mi.mesh->load(); + mi.entity = mSceneMgr->createEntity(mi.mesh); + mi.sceneNode->attachObject(mi.entity); + + mLoadingTotalMillis += mTimer.getMilliseconds() - startMillis; + mNumLoadedMeshes++; +} + +void Sample_ThreadedResourcePrep::performThreadedPrep() +{ + mNumLoadedMeshes = 0; + // mBatchSize must remain unchanged until all meshes are spawned! + mBatchSlider->getOverlayElement()->setVisible(false); + // No [*load] button must be pressed while working. + mUnloadBtn->getOverlayElement()->setVisible(false); + mReloadBtn->getOverlayElement()->setVisible(false); + for (size_t i = 0; i < mBatchSize; i++) { MeshInfo& mi = mMeshQueue[i]; - mi.mesh->load(); - mi.entity = mSceneMgr->createEntity(mi.mesh); - mi.sceneNode->attachObject(mi.entity); + ResourceBackgroundQueue::getSingleton().prepare(mi.mesh); } - long long end = mTimer.getMilliseconds(); +} - mStats.recordReloadTime(static_cast(end - start) * 0.001); +void Sample_ThreadedResourcePrep::preparingComplete(Resource* resource) +{ + for (size_t i = 0; i < mBatchSize; i++) + { + MeshInfo& mi = mMeshQueue[i]; + if (mi.meshName == resource->getName()) + { + loadMeshOnQueue(i); + break; + } + } + + if (mNumLoadedMeshes == mBatchSize) + { + // We're done! + mBatchSlider->getOverlayElement()->setVisible(true); + mUnloadBtn->getOverlayElement()->setVisible(true); + mReloadBtn->getOverlayElement()->setVisible(true); + mStats.recordReloadTime(static_cast(mLoadingTotalMillis) * 0.001); + } } void Sample_ThreadedResourcePrep::buttonHit(Button* button) @@ -242,7 +295,16 @@ void Sample_ThreadedResourcePrep::buttonHit(Button* button) if (button == mReloadBtn) { performSyncUnload(); - performSyncLoad(); + mLoadingTotalMillis = 0; + if (mThreadedMeshChk->isChecked()) + { + performThreadedPrep(); + } + else + { + performSyncPrep(); + } + mStats.recordReloadTime(static_cast(mLoadingTotalMillis) * 0.001); refreshStatsUi(); } else if (button == mUnloadBtn) @@ -251,6 +313,15 @@ void Sample_ThreadedResourcePrep::buttonHit(Button* button) } } +void Sample_ThreadedResourcePrep::checkBoxToggled(CheckBox* box) +{ + if (box == mThreadedMeshChk) + { + mStats.resetStats(); + refreshStatsUi(); + } +} + void Sample_ThreadedResourcePrep::forceUnloadAllDependentTextures(Pass* pass) { for (TextureUnitState* tus: pass->getTextureUnitStates()) From a1e99c7f9d5b304da00879c0d5bf27d5f1eebf9d Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Fri, 26 Sep 2025 00:47:36 +0200 Subject: [PATCH 08/13] StringUtil::format, corrected stats for threaded case --- .../src/ThreadedResourcePrep.cpp | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp index 7009376a070..10e34df810a 100644 --- a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -14,9 +14,6 @@ same license as the rest of the engine. #include "ThreadedResourcePrep.h" -#include -#include - Sample_ThreadedResourcePrep::Sample_ThreadedResourcePrep() { mInfo["Title"] = "Threaded Resource Prep"; @@ -82,7 +79,7 @@ void Sample_ThreadedResourcePrep::defineSelectableMesh(String name, Vector3 pos, mi.meshYaw = yaw; // Start with all meshes in scene - mi.mesh = MeshManager::getSingleton().load(name, ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + mi.mesh = MeshManager::getSingleton().load(name, RGN_DEFAULT); mi.mesh->addListener(this); mi.entity = mSceneMgr->createEntity(mi.mesh); mi.sceneNode->attachObject(mi.entity); @@ -190,11 +187,9 @@ void Sample_ThreadedResourcePrep::sliderMoved(Slider* slider) void Sample_ThreadedResourcePrep::refreshStatsUi() { - std::stringstream buf; - buf << "Reloads: " << mStats.totalReloads - << ", Avg. time: " << std::setprecision(3) << mStats.avgReloadTime - << " (Last: " << std::setprecision(3) << mStats.lastReloadTime << ")"; - mMeshStatLabel->setCaption(buf.str()); + mMeshStatLabel->setCaption( + StringUtil::format("Reloads: %d, Avg. time: %.3f (Last: %.3f)", + mStats.totalReloads, mStats.avgReloadTime, mStats.lastReloadTime)); } void Sample_ThreadedResourcePrep::refreshMeshUi() @@ -213,7 +208,7 @@ void Sample_ThreadedResourcePrep::refreshMeshUi() } } - mReloadBtn->setCaption("Reload " + StringConverter::toString((int)mBatchSlider->getValue()) + " mesh(es)"); + mReloadBtn->setCaption(StringUtil::format("Reload %d mesh(es)", (int)mBatchSlider->getValue())); } void Sample_ThreadedResourcePrep::performSyncUnload() @@ -237,6 +232,9 @@ void Sample_ThreadedResourcePrep::performSyncPrep() { loadMeshOnQueue(i); } + + mStats.recordReloadTime(static_cast(mLoadingTotalMillis) * 0.001); + refreshStatsUi(); } void Sample_ThreadedResourcePrep::loadMeshOnQueue(size_t i) @@ -287,6 +285,7 @@ void Sample_ThreadedResourcePrep::preparingComplete(Resource* resource) mUnloadBtn->getOverlayElement()->setVisible(true); mReloadBtn->getOverlayElement()->setVisible(true); mStats.recordReloadTime(static_cast(mLoadingTotalMillis) * 0.001); + refreshStatsUi(); } } @@ -304,8 +303,6 @@ void Sample_ThreadedResourcePrep::buttonHit(Button* button) { performSyncPrep(); } - mStats.recordReloadTime(static_cast(mLoadingTotalMillis) * 0.001); - refreshStatsUi(); } else if (button == mUnloadBtn) { From cd1c47d6696b5ee2310299a5a978d874b605a797 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Fri, 26 Sep 2025 03:28:32 +0200 Subject: [PATCH 09/13] Mesh: added `copyPreparedMeshFileData()` utility. /** Copy out the raw data fetched from disk after resource preparation completes (state `LOADSTATE_PREPARED`). * For advanced users only - you can manually parse the mesh data to retrieve linked resources (materials & skeleton). * @returns Mesh file data if in state `LOADSTATE_PREPARED`, otherwise null. */ --- OgreMain/include/OgreMesh.h | 6 ++++++ OgreMain/src/OgreMesh.cpp | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/OgreMain/include/OgreMesh.h b/OgreMain/include/OgreMesh.h index 2be90dc7e46..203c20b6a45 100644 --- a/OgreMain/include/OgreMesh.h +++ b/OgreMain/include/OgreMesh.h @@ -937,6 +937,12 @@ namespace Ogre { UserObjectBindings& getUserObjectBindings() { return mUserObjectBindings; } /// @overload const UserObjectBindings& getUserObjectBindings() const { return mUserObjectBindings; } + + /** Copy out the raw data fetched from disk after resource preparation completes (state `LOADSTATE_PREPARED`). + * For advanced users only - you can manually parse the mesh data to retrieve linked resources (materials & skeleton). + * @returns Mesh file data if in state `LOADSTATE_PREPARED`, otherwise null. + */ + DataStreamPtr copyPreparedMeshFileData(); }; /** A way of recording the way each LODs is recorded this Mesh. */ diff --git a/OgreMain/src/OgreMesh.cpp b/OgreMain/src/OgreMesh.cpp index c8c8c891789..ca9b4f9eec4 100644 --- a/OgreMain/src/OgreMesh.cpp +++ b/OgreMain/src/OgreMesh.cpp @@ -2362,5 +2362,16 @@ namespace Ogre { if (s->vertexData) s->vertexData->convertVertexElement(semantic, dstType); } + + DataStreamPtr Mesh::copyPreparedMeshFileData() + { + DataStreamPtr ret; + if (mLoadingState == Resource::LoadingState::LOADSTATE_PREPARED) + { + ret = DataStreamPtr(new MemoryDataStream(mFreshFromDisk)); + mFreshFromDisk->seek(0); + } + return ret; + } } From 81cbcb6aa647d3af779ea9b71e03ca6946aeb58c Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Fri, 26 Sep 2025 03:28:56 +0200 Subject: [PATCH 10/13] ThreadedResourcePrep: Working `EarlyMeshAnalyzer`! from Ogre.log: 03:19:39: [ThreadedResourcePrep]facial.mesh: skeletons:0, materials:5 03:19:39: [ThreadedResourcePrep]penguin.meshskeletons:1, materials:1 03:19:39: [ThreadedResourcePrep]knot.mesh: skeletons:0, materials:1 03:19:39: [ThreadedResourcePrep]sibenik.mesh: skeletons:0, materials:18 03:19:39: [ThreadedResourcePrep]ninja.mesh: skeletons:1, materials:2 03:19:39: [ThreadedResourcePrep]spine.mesh: skeletons:1, materials:1 03:19:39: [ThreadedResourcePrep]fish.mesh: skeletons:1, materials:1 03:19:39: [ThreadedResourcePrep]jaiqua.mesh: skeletons:1, materials:1 03:19:39: [ThreadedResourcePrep]robot.mesh: skeletons:1, materials:1 03:19:39: [ThreadedResourcePrep]Sword.mesh: skeletons:0, materials:4 03:19:39: [ThreadedResourcePrep]Barrel.mesh: skeletons:0, materials:1 03:19:39: [ThreadedResourcePrep]Sinbad.mesh: skeletons:1, materials:7 03:19:39: [ThreadedResourcePrep]DamagedHelmet.mesh: skeletons:0, materials:1 03:19:40: [ThreadedResourcePrep]razor.mesh: skeletons:0, materials:1 03:19:40: [ThreadedResourcePrep]geosphere4500.mesh: skeletons:0, materials:1 03:19:40: [ThreadedResourcePrep]dragon.mesh: skeletons:0, materials:3 03:19:40: [ThreadedResourcePrep]geosphere8000.mesh: skeletons:0, materials:1 --- .../include/ThreadedResourcePrep.h | 16 ++ .../src/ThreadedResourcePrep.cpp | 147 ++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h index eb43e81fb23..188f133e93b 100644 --- a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h +++ b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h @@ -58,6 +58,22 @@ struct Stats } }; +// Materials and skeleton linked from mesh file aren't known until the mesh is `load()`-ed. +// `MeshSerializerImpl` takes care of properly parsing the binary mesh format. +// To find the resources early (so we can `prepare()` them in time), we're on our own. +struct EarlyMeshAnalyzer: public Serializer +{ + EarlyMeshAnalyzer(); + void discoverLinkedResources(MeshPtr& mesh); + + StringVector materialNames; + StringVector skeletonNames; + bool skeletallyAnimated = false; + +protected: + void readMesh(DataStreamPtr& stream); +}; + class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample, public Resource::Listener { public: diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp index 10e34df810a..1e3efd7125f 100644 --- a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -242,6 +242,7 @@ void Sample_ThreadedResourcePrep::loadMeshOnQueue(size_t i) long long startMillis = mTimer.getMilliseconds(); MeshInfo& mi = mMeshQueue[i]; + mi.mesh->load(); mi.entity = mSceneMgr->createEntity(mi.mesh); mi.sceneNode->attachObject(mi.entity); @@ -273,6 +274,21 @@ void Sample_ThreadedResourcePrep::preparingComplete(Resource* resource) MeshInfo& mi = mMeshQueue[i]; if (mi.meshName == resource->getName()) { + //"##### BEGIN TEST of the early analyzer #####\n"; + + if (mi.mesh->getLoadingState() != Resource::LOADSTATE_PREPARED) + { + Ogre::LogManager::getSingleton().stream() << "[ThreadedResourcePrep]" << mi.meshName << " is not in LOADSTATE_PREPARED, but: " << mi.mesh->getLoadingState(); + } + else + { + EarlyMeshAnalyzer analyzer; + analyzer.discoverLinkedResources(mi.mesh); + Ogre::LogManager::getSingleton().stream() << "[ThreadedResourcePrep]" << mi.meshName << ": skeletons:" << analyzer.skeletonNames.size() << ", materials:" << analyzer.materialNames.size(); + } + + // "##### END TEST of the early analyzer #####\n"; + loadMeshOnQueue(i); break; } @@ -351,3 +367,134 @@ void Sample_ThreadedResourcePrep::forceUnloadAllDependentResources(MeshPtr& mesh } } } + +EarlyMeshAnalyzer::EarlyMeshAnalyzer() +{ + mVersion = "[MeshSerializer_v1.100]"; // see `MeshSerializerImpl::MeshSerializerImpl()` +} + +enum EearlyMeshChunkID // see internal header 'OgreMeshFileFormat.h' +{ + M_HEADER = 0x1000, + M_MESH = 0x3000, + M_SUBMESH = 0x4000, + M_GEOMETRY = 0x5000, + M_MESH_SKELETON_LINK = 0x6000, + M_MESH_BONE_ASSIGNMENT = 0x7000, + M_MESH_LOD_LEVEL = 0x8000, + M_MESH_BOUNDS = 0x9000, + M_SUBMESH_NAME_TABLE = 0xA000, + M_EDGE_LISTS = 0xB000, + M_POSES = 0xC000, + M_ANIMATIONS = 0xD000, + M_TABLE_EXTREMES = 0xE000, +}; + +void EarlyMeshAnalyzer::discoverLinkedResources(MeshPtr& mesh) +{ + // Materials and skeleton aren't known until the mesh is `load()`-ed. + // `MeshSerializerImpl` takes care of properly parsing the binary mesh format. + // To find the info early, we're on our own. + DataStreamPtr stream = mesh->copyPreparedMeshFileData(); + stream->seek(0); + + // ----- see `MeshSerializerImpl::importMesh()` ----- + determineEndianness(stream); + readFileHeader(stream); + pushInnerChunk(stream); + + unsigned short streamID = readChunk(stream); + while(!stream->eof()) + { + switch (streamID) + { + case M_MESH: + readMesh(stream); + break; + } + + streamID = readChunk(stream); + } + + popInnerChunk(stream); +} + +/// stream overhead = ID + size +const long MSTREAM_OVERHEAD_SIZE = sizeof(uint16) + sizeof(uint32); + +void EarlyMeshAnalyzer::readMesh(DataStreamPtr& stream) +{ + // ----- see `MeshSerializerImpl::readMesh()` ----- + readBools(stream, &skeletallyAnimated, 1); + + // Find all substreams + if (!stream->eof()) + { + pushInnerChunk(stream); + unsigned short streamID = readChunk(stream); + while(!stream->eof() && + (streamID == M_GEOMETRY || + streamID == M_SUBMESH || + streamID == M_MESH_SKELETON_LINK || + streamID == M_MESH_BONE_ASSIGNMENT || + streamID == M_MESH_LOD_LEVEL || + streamID == M_MESH_BOUNDS || + streamID == M_SUBMESH_NAME_TABLE || + streamID == M_EDGE_LISTS || + streamID == M_POSES || + streamID == M_ANIMATIONS || + streamID == M_TABLE_EXTREMES)) + { + size_t dataStartPos = stream->tell(); + switch(streamID) + { + case M_GEOMETRY: + stream->skip(mCurrentstreamLen - MSTREAM_OVERHEAD_SIZE); + break; + case M_SUBMESH: + materialNames.push_back(readString(stream)); // see `MeshSerializerImpl::readSubMesh()` + stream->skip(mCurrentstreamLen - (MSTREAM_OVERHEAD_SIZE + (stream->tell() - dataStartPos))); + break; + case M_MESH_SKELETON_LINK: + skeletonNames.push_back(readString(stream)); // see `MeshSerializerImpl::readSkeletonLink()` + break; + case M_MESH_BONE_ASSIGNMENT: + stream->skip(mCurrentstreamLen - MSTREAM_OVERHEAD_SIZE); + break; + case M_MESH_LOD_LEVEL: + stream->skip(mCurrentstreamLen - MSTREAM_OVERHEAD_SIZE); + break; + case M_MESH_BOUNDS: + stream->skip(mCurrentstreamLen - MSTREAM_OVERHEAD_SIZE); + break; + case M_SUBMESH_NAME_TABLE: + stream->skip(mCurrentstreamLen - MSTREAM_OVERHEAD_SIZE); + break; + case M_EDGE_LISTS: + stream->skip(mCurrentstreamLen - MSTREAM_OVERHEAD_SIZE); + break; + case M_POSES: + stream->skip(mCurrentstreamLen - MSTREAM_OVERHEAD_SIZE); + break; + case M_ANIMATIONS: + stream->skip(mCurrentstreamLen - MSTREAM_OVERHEAD_SIZE); + break; + case M_TABLE_EXTREMES: + stream->skip(mCurrentstreamLen - MSTREAM_OVERHEAD_SIZE); + break; + } + + if (!stream->eof()) + { + streamID = readChunk(stream); + } + + } + if (!stream->eof()) + { + // Backpedal back to start of stream + backpedalChunkHeader(stream); + } + popInnerChunk(stream); + } +} From 19f948e573a20b8a5cf60bc28cf4489dba2a3aad Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Fri, 26 Sep 2025 05:18:46 +0200 Subject: [PATCH 11/13] ThreadedResourcePrep: textures & skeletons also bg! --- .../include/ThreadedResourcePrep.h | 28 +++- .../src/ThreadedResourcePrep.cpp | 144 ++++++++++++++---- 2 files changed, 143 insertions(+), 29 deletions(-) diff --git a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h index 188f133e93b..ea91414be4d 100644 --- a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h +++ b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h @@ -24,6 +24,8 @@ same license as the rest of the engine. using namespace Ogre; using namespace OgreBites; +class Sample_ThreadedResourcePrep; + struct MeshInfo { // Each resource has dedicated spot, so they can appear in any order. @@ -74,11 +76,32 @@ struct EarlyMeshAnalyzer: public Serializer void readMesh(DataStreamPtr& stream); }; +class LinkedResourcePrepper: public Resource::Listener +{ +public: + LinkedResourcePrepper(size_t queuePos, Sample_ThreadedResourcePrep* goal); + ~LinkedResourcePrepper(); + + void hookResource(ResourcePtr resource); + void startPreparing(); + + // Resource listener + void preparingComplete(Resource*) override; + +private: + size_t mQueuePos = 0; + std::vector mHookedResources; + Sample_ThreadedResourcePrep* mGoal = nullptr; + size_t mNumPrepared = 0; // accumulator +}; + class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample, public Resource::Listener { public: Sample_ThreadedResourcePrep(); + void loadMeshOnQueue(size_t i); + private: // SdkSample setup void setupContent() override; @@ -95,19 +118,21 @@ class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample, pub // Custom UI void refreshStatsUi(); void refreshMeshUi(); + void setThreadingUiVisible(bool vis); // The heavy lifting void performSyncUnload(); void performSyncPrep(); - void loadMeshOnQueue(size_t i); void performThreadedPrep(); static void forceUnloadAllDependentResources(MeshPtr& mesh); static void forceUnloadAllDependentTextures(Pass* pass); + void requestLinkedResourcesThreadedPrep(size_t i); // Resource listener void preparingComplete(Resource*) override; std::deque mMeshQueue; + std::vector> mPreppers; SelectMenu* mMeshMenu; Label* mMeshStatLabel; Stats mStats; @@ -117,6 +142,7 @@ class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample, pub size_t mBatchSize = 0; Timer mTimer; CheckBox* mThreadedMeshChk; + CheckBox* mThreadedAllChk; // accumulators of `loadMeshOnQueue()`: long long mLoadingTotalMillis; diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp index 1e3efd7125f..c40be6ec962 100644 --- a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -133,6 +133,7 @@ void Sample_ThreadedResourcePrep::setupControls() mBatchSlider = mTrayMgr->createThickSlider(TL_TOPLEFT, "BatchSlider", "Batch length", TOPLEFT_W, 50, 1, (Real)mMeshQueue.size(), (int)mMeshQueue.size()); mThreadedMeshChk = mTrayMgr->createCheckBox(TL_TOPLEFT, "ThrMeshChk", "Bg. mesh prep", TOPLEFT_W); + mThreadedAllChk = mTrayMgr->createCheckBox(TL_TOPLEFT, "ThrAllChk", "Bg. all prep", TOPLEFT_W); mUnloadBtn = mTrayMgr->createButton(TL_TOPLEFT, "UnloadBtn", "Unload batch", TOPLEFT_W); @@ -232,9 +233,6 @@ void Sample_ThreadedResourcePrep::performSyncPrep() { loadMeshOnQueue(i); } - - mStats.recordReloadTime(static_cast(mLoadingTotalMillis) * 0.001); - refreshStatsUi(); } void Sample_ThreadedResourcePrep::loadMeshOnQueue(size_t i) @@ -249,16 +247,32 @@ void Sample_ThreadedResourcePrep::loadMeshOnQueue(size_t i) mLoadingTotalMillis += mTimer.getMilliseconds() - startMillis; mNumLoadedMeshes++; + + if (mNumLoadedMeshes == mBatchSize) + { + // We're done! + setThreadingUiVisible(true); + mStats.recordReloadTime(static_cast(mLoadingTotalMillis) * 0.001); + refreshStatsUi(); + } } -void Sample_ThreadedResourcePrep::performThreadedPrep() +void Sample_ThreadedResourcePrep::setThreadingUiVisible(bool vis) { - mNumLoadedMeshes = 0; // mBatchSize must remain unchanged until all meshes are spawned! - mBatchSlider->getOverlayElement()->setVisible(false); + mBatchSlider->getOverlayElement()->setVisible(vis); // No [*load] button must be pressed while working. - mUnloadBtn->getOverlayElement()->setVisible(false); - mReloadBtn->getOverlayElement()->setVisible(false); + mUnloadBtn->getOverlayElement()->setVisible(vis); + mReloadBtn->getOverlayElement()->setVisible(vis); + // mThreadedAllChk must remain unchanged while working + mThreadedAllChk->getOverlayElement()->setVisible(vis); +} + +void Sample_ThreadedResourcePrep::performThreadedPrep() +{ + mNumLoadedMeshes = 0; + mPreppers.clear(); + setThreadingUiVisible(false); for (size_t i = 0; i < mBatchSize; i++) { @@ -274,35 +288,63 @@ void Sample_ThreadedResourcePrep::preparingComplete(Resource* resource) MeshInfo& mi = mMeshQueue[i]; if (mi.meshName == resource->getName()) { - //"##### BEGIN TEST of the early analyzer #####\n"; - - if (mi.mesh->getLoadingState() != Resource::LOADSTATE_PREPARED) + if (!mThreadedAllChk->isChecked()) { - Ogre::LogManager::getSingleton().stream() << "[ThreadedResourcePrep]" << mi.meshName << " is not in LOADSTATE_PREPARED, but: " << mi.mesh->getLoadingState(); + loadMeshOnQueue(i); } else { - EarlyMeshAnalyzer analyzer; - analyzer.discoverLinkedResources(mi.mesh); - Ogre::LogManager::getSingleton().stream() << "[ThreadedResourcePrep]" << mi.meshName << ": skeletons:" << analyzer.skeletonNames.size() << ", materials:" << analyzer.materialNames.size(); + requestLinkedResourcesThreadedPrep(i); } - - // "##### END TEST of the early analyzer #####\n"; - - loadMeshOnQueue(i); break; } } +} - if (mNumLoadedMeshes == mBatchSize) +void Sample_ThreadedResourcePrep::requestLinkedResourcesThreadedPrep(size_t i) +{ + MeshInfo& mi = mMeshQueue[i]; + if (mi.mesh->getLoadingState() != Resource::LOADSTATE_PREPARED) { - // We're done! - mBatchSlider->getOverlayElement()->setVisible(true); - mUnloadBtn->getOverlayElement()->setVisible(true); - mReloadBtn->getOverlayElement()->setVisible(true); - mStats.recordReloadTime(static_cast(mLoadingTotalMillis) * 0.001); - refreshStatsUi(); + Ogre::LogManager::getSingleton().stream() << "[ThreadedResourcePrep]" << mi.meshName << " is not in LOADSTATE_PREPARED, but: " << mi.mesh->getLoadingState(); + return; + } + + EarlyMeshAnalyzer analyzer; + analyzer.discoverLinkedResources(mi.mesh); + Ogre::LogManager::getSingleton().stream() << "[ThreadedResourcePrep]" << mi.meshName << ": skeletons:" << analyzer.skeletonNames.size() << ", materials:" << analyzer.materialNames.size(); + + LinkedResourcePrepper* prepper = new LinkedResourcePrepper(i, this); + for (String const& skeletonName: analyzer.skeletonNames) + { + prepper->hookResource(Ogre::SkeletonManager::getSingleton().getResourceByName(skeletonName)); + } + for (String const& materialName: analyzer.materialNames) + { + MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName(materialName); + if (mat) + { + for (Technique* teq: mat->getSupportedTechniques()) + { + for (Pass* pass: teq->getPasses()) + { + for (TextureUnitState* tus: pass->getTextureUnitStates()) + { + for (size_t iFrame = 0; iFrame < tus->getNumFrames(); iFrame++) + { + TexturePtr tex = Ogre::TextureManager::getSingleton().getByName(tus->getFrameTextureName(iFrame)); + if (tex) + { + prepper->hookResource(tex); + } + } + } + } + } + } } + prepper->startPreparing(); + mPreppers.push_back(std::unique_ptr(prepper)); } void Sample_ThreadedResourcePrep::buttonHit(Button* button) @@ -328,7 +370,7 @@ void Sample_ThreadedResourcePrep::buttonHit(Button* button) void Sample_ThreadedResourcePrep::checkBoxToggled(CheckBox* box) { - if (box == mThreadedMeshChk) + if (box == mThreadedMeshChk || box == mThreadedAllChk) { mStats.resetStats(); refreshStatsUi(); @@ -396,7 +438,6 @@ void EarlyMeshAnalyzer::discoverLinkedResources(MeshPtr& mesh) // `MeshSerializerImpl` takes care of properly parsing the binary mesh format. // To find the info early, we're on our own. DataStreamPtr stream = mesh->copyPreparedMeshFileData(); - stream->seek(0); // ----- see `MeshSerializerImpl::importMesh()` ----- determineEndianness(stream); @@ -498,3 +539,50 @@ void EarlyMeshAnalyzer::readMesh(DataStreamPtr& stream) popInnerChunk(stream); } } + + +LinkedResourcePrepper::LinkedResourcePrepper(size_t queuePos, Sample_ThreadedResourcePrep* goal): + mQueuePos(queuePos), + mGoal(goal) +{ +} + +LinkedResourcePrepper::~LinkedResourcePrepper() +{ + for (ResourcePtr& resource: mHookedResources) + { + resource->removeListener(this); + } +} + +void LinkedResourcePrepper::hookResource(ResourcePtr resource) +{ + resource->addListener(this); + mHookedResources.push_back(resource); +} + +void LinkedResourcePrepper::startPreparing() +{ + if (mHookedResources.size() == 0) + { + // Nothing to do + mGoal->loadMeshOnQueue(mQueuePos); + return; + } + + mNumPrepared = 0; + for (ResourcePtr& resource: mHookedResources) + { + ResourceBackgroundQueue::getSingleton().prepare(resource); + } +} + +// Resource listener +void LinkedResourcePrepper::preparingComplete(Resource*) +{ + mNumPrepared++; + if (mNumPrepared == mHookedResources.size()) + { + mGoal->loadMeshOnQueue(mQueuePos); + } +} From 7fe92802939df8c6b6c718afe20934b5070c732e Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Tue, 7 Oct 2025 23:08:54 +0200 Subject: [PATCH 12/13] Added "early discovery" checkbox + thumbnail. This reverts the `Mesh::copyPreparedMeshFileData()` hack ~ commit cd1c47d6696b5ee2310299a5a978d874b605a797. However, the early discovery logic is kept, the mesh file is simply loaded twice. To double check load()-ing the mesh doesn't prepare any other resources, the reload test can be performed without the early discovery where mesh gets simply load()-ed, then background prep of textures/skeletons is queried. --- OgreMain/include/OgreMesh.h | 6 -- OgreMain/src/OgreMesh.cpp | 11 ---- Samples/Media/thumbnails/thumb_bgprep.png | Bin 0 -> 35125 bytes .../include/ThreadedResourcePrep.h | 9 ++- .../src/ThreadedResourcePrep.cpp | 52 +++++++++++++++--- 5 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 Samples/Media/thumbnails/thumb_bgprep.png diff --git a/OgreMain/include/OgreMesh.h b/OgreMain/include/OgreMesh.h index 203c20b6a45..2be90dc7e46 100644 --- a/OgreMain/include/OgreMesh.h +++ b/OgreMain/include/OgreMesh.h @@ -937,12 +937,6 @@ namespace Ogre { UserObjectBindings& getUserObjectBindings() { return mUserObjectBindings; } /// @overload const UserObjectBindings& getUserObjectBindings() const { return mUserObjectBindings; } - - /** Copy out the raw data fetched from disk after resource preparation completes (state `LOADSTATE_PREPARED`). - * For advanced users only - you can manually parse the mesh data to retrieve linked resources (materials & skeleton). - * @returns Mesh file data if in state `LOADSTATE_PREPARED`, otherwise null. - */ - DataStreamPtr copyPreparedMeshFileData(); }; /** A way of recording the way each LODs is recorded this Mesh. */ diff --git a/OgreMain/src/OgreMesh.cpp b/OgreMain/src/OgreMesh.cpp index ca9b4f9eec4..c8c8c891789 100644 --- a/OgreMain/src/OgreMesh.cpp +++ b/OgreMain/src/OgreMesh.cpp @@ -2362,16 +2362,5 @@ namespace Ogre { if (s->vertexData) s->vertexData->convertVertexElement(semantic, dstType); } - - DataStreamPtr Mesh::copyPreparedMeshFileData() - { - DataStreamPtr ret; - if (mLoadingState == Resource::LoadingState::LOADSTATE_PREPARED) - { - ret = DataStreamPtr(new MemoryDataStream(mFreshFromDisk)); - mFreshFromDisk->seek(0); - } - return ret; - } } diff --git a/Samples/Media/thumbnails/thumb_bgprep.png b/Samples/Media/thumbnails/thumb_bgprep.png new file mode 100644 index 0000000000000000000000000000000000000000..aa70f7759c8406070d6626d1fbecb34f8218a258 GIT binary patch literal 35125 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, "ThrMeshChk", "Bg. mesh prep", TOPLEFT_W); mThreadedAllChk = mTrayMgr->createCheckBox(TL_TOPLEFT, "ThrAllChk", "Bg. all prep", TOPLEFT_W); + mEarlyDiscoveryChk = mTrayMgr->createCheckBox(TL_TOPLEFT, "EarlyDiscovChk", "Early discovery", TOPLEFT_W); mUnloadBtn = mTrayMgr->createButton(TL_TOPLEFT, "UnloadBtn", "Unload batch", TOPLEFT_W); @@ -257,6 +259,17 @@ void Sample_ThreadedResourcePrep::loadMeshOnQueue(size_t i) } } +void Sample_ThreadedResourcePrep::loadMeshOnQueueInAdvance(size_t i) +{ + long long startMillis = mTimer.getMilliseconds(); + + MeshInfo& mi = mMeshQueue[i]; + + mi.mesh->load(); + + mLoadingTotalMillis += mTimer.getMilliseconds() - startMillis; +} + void Sample_ThreadedResourcePrep::setThreadingUiVisible(bool vis) { // mBatchSize must remain unchanged until all meshes are spawned! @@ -310,8 +323,16 @@ void Sample_ThreadedResourcePrep::requestLinkedResourcesThreadedPrep(size_t i) return; } - EarlyMeshAnalyzer analyzer; - analyzer.discoverLinkedResources(mi.mesh); + LinkedResourceFinder analyzer; + if (mEarlyDiscoveryChk->isChecked()) + { + analyzer.discoverLinkedResourcesEarly(mi.mesh); + } + else + { + loadMeshOnQueueInAdvance(i); + analyzer.gatherLinkedResourcesPostLoad(mi.mesh); + } Ogre::LogManager::getSingleton().stream() << "[ThreadedResourcePrep]" << mi.meshName << ": skeletons:" << analyzer.skeletonNames.size() << ", materials:" << analyzer.materialNames.size(); LinkedResourcePrepper* prepper = new LinkedResourcePrepper(i, this); @@ -370,7 +391,7 @@ void Sample_ThreadedResourcePrep::buttonHit(Button* button) void Sample_ThreadedResourcePrep::checkBoxToggled(CheckBox* box) { - if (box == mThreadedMeshChk || box == mThreadedAllChk) + if (box == mThreadedMeshChk || box == mThreadedAllChk || box == mEarlyDiscoveryChk) { mStats.resetStats(); refreshStatsUi(); @@ -410,7 +431,7 @@ void Sample_ThreadedResourcePrep::forceUnloadAllDependentResources(MeshPtr& mesh } } -EarlyMeshAnalyzer::EarlyMeshAnalyzer() +LinkedResourceFinder::LinkedResourceFinder() { mVersion = "[MeshSerializer_v1.100]"; // see `MeshSerializerImpl::MeshSerializerImpl()` } @@ -432,12 +453,29 @@ enum EearlyMeshChunkID // see internal header 'OgreMeshFileFormat.h' M_TABLE_EXTREMES = 0xE000, }; -void EarlyMeshAnalyzer::discoverLinkedResources(MeshPtr& mesh) +void LinkedResourceFinder::gatherLinkedResourcesPostLoad(MeshPtr& mesh) +{ + // this is just convenience to use the same dispatch code later on. + + if (mesh->hasSkeleton()) + { + skeletallyAnimated=true; + skeletonNames.push_back(mesh->getSkeletonName()); + } + + for (SubMesh* submesh: mesh->getSubMeshes()) + { + materialNames.push_back(submesh->getMaterialName()); + } +} + +void LinkedResourceFinder::discoverLinkedResourcesEarly(MeshPtr& mesh) { // Materials and skeleton aren't known until the mesh is `load()`-ed. // `MeshSerializerImpl` takes care of properly parsing the binary mesh format. // To find the info early, we're on our own. - DataStreamPtr stream = mesh->copyPreparedMeshFileData(); + // Even if the mesh is prepared, there's no way to get the buffered data - we must load it ourselves. + DataStreamPtr stream = Ogre::ResourceGroupManager::getSingleton().openResource(mesh->getName(), mesh->getGroup(), mesh.get()); // ----- see `MeshSerializerImpl::importMesh()` ----- determineEndianness(stream); @@ -463,7 +501,7 @@ void EarlyMeshAnalyzer::discoverLinkedResources(MeshPtr& mesh) /// stream overhead = ID + size const long MSTREAM_OVERHEAD_SIZE = sizeof(uint16) + sizeof(uint32); -void EarlyMeshAnalyzer::readMesh(DataStreamPtr& stream) +void LinkedResourceFinder::readMesh(DataStreamPtr& stream) { // ----- see `MeshSerializerImpl::readMesh()` ----- readBools(stream, &skeletallyAnimated, 1); From d8aeaba72e42640ce767ac44a63eec3a2419ba86 Mon Sep 17 00:00:00 2001 From: ohlidalp Date: Thu, 9 Oct 2025 00:44:50 +0200 Subject: [PATCH 13/13] added "Threaded early discovery" mode To enable it, tick all the checkboxes (to be cleaned up). This means the mesh is prepared via ResourceBackgroundQueue, then a custom task is queued to WorkQueue which: 1. loads the mesh file again, 2. discovers skeletons+textures using custom code 3. and then queues all of them via ResourceBackgroundQueue. I expected to see almost no FPS spike with this approach as everything is both fetched to RAM and analyzed on background, only the final mesh loading (which constructs actual hardware buffers) is done on foreground. However, the results are basically identical to just loading the mesh on foreground first and then queue textures+skeletons via ResourceBackgroundQueue. --- .../include/ThreadedResourcePrep.h | 4 +- .../src/ThreadedResourcePrep.cpp | 107 ++++++++++++------ 2 files changed, 75 insertions(+), 36 deletions(-) diff --git a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h index b908ad7c6d8..f7e3801c8f4 100644 --- a/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h +++ b/Samples/ThreadedResourcePrep/include/ThreadedResourcePrep.h @@ -80,7 +80,7 @@ struct LinkedResourceFinder: public Serializer class LinkedResourcePrepper: public Resource::Listener { public: - LinkedResourcePrepper(size_t queuePos, Sample_ThreadedResourcePrep* goal); + LinkedResourcePrepper(size_t queuePos, Sample_ThreadedResourcePrep* goal, LinkedResourceFinder& analyzer); ~LinkedResourcePrepper(); void hookResource(ResourcePtr resource); @@ -103,6 +103,7 @@ class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample, pub void loadMeshOnQueue(size_t i); void loadMeshOnQueueInAdvance(size_t i); + void addPrepper(LinkedResourcePrepper* prepperPtr); private: // SdkSample setup @@ -146,6 +147,7 @@ class _OgreSampleClassExport Sample_ThreadedResourcePrep : public SdkSample, pub CheckBox* mThreadedMeshChk; CheckBox* mThreadedAllChk; CheckBox* mEarlyDiscoveryChk; + CheckBox* mThrEarlyDiscoveryChk; // accumulators of `loadMeshOnQueue()`: long long mLoadingTotalMillis; diff --git a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp index cc7bb5e1202..29f1c0f088e 100644 --- a/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp +++ b/Samples/ThreadedResourcePrep/src/ThreadedResourcePrep.cpp @@ -48,6 +48,13 @@ void Sample_ThreadedResourcePrep::setupContent() floor->setCastShadows(false); mSceneMgr->getRootSceneNode()->attachObject(floor); + // add a particle system to better feel the FPS spikes when reloading. + ParticleSystem* psys = mSceneMgr->createParticleSystem("Particles", "Examples/PurpleFountain"); + psys->setKeepParticlesInLocalSpace(true); + SceneNode* pnode = mSceneMgr->getRootSceneNode()->createChildSceneNode(Vector3(0,5,-35)); + pnode->attachObject(psys); + pnode->setScale(Vector3(0.5,0.1, 0.5)); + setupSelectableMeshes(); setupControls(); @@ -136,6 +143,7 @@ void Sample_ThreadedResourcePrep::setupControls() mThreadedMeshChk = mTrayMgr->createCheckBox(TL_TOPLEFT, "ThrMeshChk", "Bg. mesh prep", TOPLEFT_W); mThreadedAllChk = mTrayMgr->createCheckBox(TL_TOPLEFT, "ThrAllChk", "Bg. all prep", TOPLEFT_W); mEarlyDiscoveryChk = mTrayMgr->createCheckBox(TL_TOPLEFT, "EarlyDiscovChk", "Early discovery", TOPLEFT_W); + mThrEarlyDiscoveryChk = mTrayMgr->createCheckBox(TL_TOPLEFT, "ThrEarlyDiscovChk", " ^ +Threaded", TOPLEFT_W); mUnloadBtn = mTrayMgr->createButton(TL_TOPLEFT, "UnloadBtn", "Unload batch", TOPLEFT_W); @@ -314,6 +322,11 @@ void Sample_ThreadedResourcePrep::preparingComplete(Resource* resource) } } +void Sample_ThreadedResourcePrep::addPrepper(LinkedResourcePrepper* prepperPtr) +{ + mPreppers.push_back(std::unique_ptr(prepperPtr)); +} + void Sample_ThreadedResourcePrep::requestLinkedResourcesThreadedPrep(size_t i) { MeshInfo& mi = mMeshQueue[i]; @@ -326,7 +339,25 @@ void Sample_ThreadedResourcePrep::requestLinkedResourcesThreadedPrep(size_t i) LinkedResourceFinder analyzer; if (mEarlyDiscoveryChk->isChecked()) { - analyzer.discoverLinkedResourcesEarly(mi.mesh); + if (mThrEarlyDiscoveryChk->isChecked()) + { + Root::getSingleton().getWorkQueue()->addTask([i, &mi, analyzer, this]() + { + LinkedResourceFinder bgAnalyzer; + bgAnalyzer.discoverLinkedResourcesEarly(mi.mesh); + LinkedResourcePrepper* prepper = new LinkedResourcePrepper(i, this, bgAnalyzer); + prepper->startPreparing(); + Root::getSingleton().getWorkQueue()->addMainThreadTask([this, prepper]() + { + this->addPrepper(prepper); + }); + }); + return; + } + else + { + analyzer.discoverLinkedResourcesEarly(mi.mesh); + } } else { @@ -335,37 +366,9 @@ void Sample_ThreadedResourcePrep::requestLinkedResourcesThreadedPrep(size_t i) } Ogre::LogManager::getSingleton().stream() << "[ThreadedResourcePrep]" << mi.meshName << ": skeletons:" << analyzer.skeletonNames.size() << ", materials:" << analyzer.materialNames.size(); - LinkedResourcePrepper* prepper = new LinkedResourcePrepper(i, this); - for (String const& skeletonName: analyzer.skeletonNames) - { - prepper->hookResource(Ogre::SkeletonManager::getSingleton().getResourceByName(skeletonName)); - } - for (String const& materialName: analyzer.materialNames) - { - MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName(materialName); - if (mat) - { - for (Technique* teq: mat->getSupportedTechniques()) - { - for (Pass* pass: teq->getPasses()) - { - for (TextureUnitState* tus: pass->getTextureUnitStates()) - { - for (size_t iFrame = 0; iFrame < tus->getNumFrames(); iFrame++) - { - TexturePtr tex = Ogre::TextureManager::getSingleton().getByName(tus->getFrameTextureName(iFrame)); - if (tex) - { - prepper->hookResource(tex); - } - } - } - } - } - } - } + LinkedResourcePrepper* prepper = new LinkedResourcePrepper(i, this, analyzer); prepper->startPreparing(); - mPreppers.push_back(std::unique_ptr(prepper)); + addPrepper(prepper); } void Sample_ThreadedResourcePrep::buttonHit(Button* button) @@ -391,7 +394,7 @@ void Sample_ThreadedResourcePrep::buttonHit(Button* button) void Sample_ThreadedResourcePrep::checkBoxToggled(CheckBox* box) { - if (box == mThreadedMeshChk || box == mThreadedAllChk || box == mEarlyDiscoveryChk) + if (box == mThreadedMeshChk || box == mThreadedAllChk || box == mEarlyDiscoveryChk || box == mThrEarlyDiscoveryChk) { mStats.resetStats(); refreshStatsUi(); @@ -579,10 +582,38 @@ void LinkedResourceFinder::readMesh(DataStreamPtr& stream) } -LinkedResourcePrepper::LinkedResourcePrepper(size_t queuePos, Sample_ThreadedResourcePrep* goal): +LinkedResourcePrepper::LinkedResourcePrepper(size_t queuePos, Sample_ThreadedResourcePrep* goal, LinkedResourceFinder& analyzer): mQueuePos(queuePos), mGoal(goal) { + for (String const& skeletonName: analyzer.skeletonNames) + { + hookResource(Ogre::SkeletonManager::getSingleton().getResourceByName(skeletonName)); + } + for (String const& materialName: analyzer.materialNames) + { + MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName(materialName); + if (mat) + { + for (Technique* teq: mat->getSupportedTechniques()) + { + for (Pass* pass: teq->getPasses()) + { + for (TextureUnitState* tus: pass->getTextureUnitStates()) + { + for (size_t iFrame = 0; iFrame < tus->getNumFrames(); iFrame++) + { + TexturePtr tex = Ogre::TextureManager::getSingleton().getByName(tus->getFrameTextureName(iFrame)); + if (tex) + { + hookResource(tex); + } + } + } + } + } + } + } } LinkedResourcePrepper::~LinkedResourcePrepper() @@ -603,8 +634,14 @@ void LinkedResourcePrepper::startPreparing() { if (mHookedResources.size() == 0) { - // Nothing to do - mGoal->loadMeshOnQueue(mQueuePos); + // No resources to load -> nobody to invoke `preparingComplete()` -> we must request loading mesh now. + // because this function is invocable on background, finish using mainThreadTask + Sample_ThreadedResourcePrep* goalPtr = mGoal; + size_t queuePos = mQueuePos; + Root::getSingleton().getWorkQueue()->addMainThreadTask([goalPtr, queuePos]() + { + goalPtr->loadMeshOnQueue(queuePos); + }); return; }