Use a coroutine for Loader tasks

Some tasks are invoked multiple times. Normally those tasks are broken
up inside a loop and had to be continued there. With coroutines that is
easier as it's possible to suspend inside a loop.

Coroutines which are lambdas should not capture anythig as the lifetime
of the captured values might end before the coroutine completes. For
that purpose `std::bind_front` is used.
This commit is contained in:
phosit 2025-10-20 21:55:24 +02:00
parent b67faf35c3
commit 5586802b86
No known key found for this signature in database
GPG key ID: C9430B600671C268
12 changed files with 421 additions and 322 deletions

View file

@ -206,20 +206,16 @@ CMiniMapTexture& CGameView::GetMiniMapTexture()
void CGameView::RegisterInit()
{
// CGameView init
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](ICameraController* cameraController) -> PS::Loader::Task
{
m->CameraController->LoadConfig();
return 0;
}, L"CGameView init", 1);
cameraController->LoadConfig();
co_return 0;
}, m->CameraController.get()), L"CGameView init", 1);
PS::Loader::Register([]
{
return g_TexMan.StartTerrainTextures();
}, L"StartTerrainTextures", 1);
PS::Loader::Register([]
{
return g_TexMan.PollTerrainTextures();
}, L"PollTerrainTextures", 60);
return g_TexMan.LoadTerrainTextures();
}, L"TerrainTextures", 61);
}
void CGameView::BeginFrame()

View file

@ -79,6 +79,7 @@
#include <atomic>
#include <functional>
#include <js/PropertyAndElement.h>
#include <ranges>
#include <string>
#include <string_view>
#include <utility>
@ -150,21 +151,21 @@ void CMapReader::LoadMap(const VfsPath& pathname, const ScriptContext& cx, JS::
// load map or script settings script
if (settings.isUndefined())
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return LoadScriptSettings();
}, L"CMapReader::LoadScriptSettings", 50);
co_return mapReader->LoadScriptSettings();
}, this), L"CMapReader::LoadScriptSettings", 50);
else
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return LoadRMSettings();
}, L"CMapReader::LoadRMSettings", 50);
co_return mapReader->LoadRMSettings();
}, this), L"CMapReader::LoadRMSettings", 50);
// load player settings script (must be done before reading map)
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return LoadPlayerSettings();
}, L"CMapReader::LoadPlayerSettings", 50);
co_return mapReader->LoadPlayerSettings();
}, this), L"CMapReader::LoadPlayerSettings", 50);
// unpack the data
if (!only_xml)
@ -174,16 +175,16 @@ void CMapReader::LoadMap(const VfsPath& pathname, const ScriptContext& cx, JS::
}, L"CMapReader::UnpackMap", 1200);
// read the corresponding XML file
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return ReadXML();
}, L"CMapReader::ReadXML", 50);
co_return mapReader->ReadXML();
}, this), L"CMapReader::ReadXML", 50);
// apply terrain data to the world
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return ApplyTerrainData();
}, L"CMapReader::ApplyTerrainData", 5);
co_return mapReader->ApplyTerrainData();
}, this), L"CMapReader::ApplyTerrainData", 5);
// read entities
PS::Loader::Register([this]
@ -192,16 +193,16 @@ void CMapReader::LoadMap(const VfsPath& pathname, const ScriptContext& cx, JS::
}, L"CMapReader::ReadXMLEntities", 5800);
// apply misc data to the world
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return ApplyData();
}, L"CMapReader::ApplyData", 5);
co_return mapReader->ApplyData();
}, this), L"CMapReader::ApplyData", 5);
// load map settings script (must be done after reading map)
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return LoadMapSettings();
}, L"CMapReader::LoadMapSettings", 5);
co_return mapReader->LoadMapSettings();
}, this), L"CMapReader::LoadMapSettings", 5);
}
// LoadRandomMap: try to load the map data; reinitialise the scene to new data if successful
@ -232,86 +233,70 @@ void CMapReader::LoadRandomMap(const CStrW& scriptFile, const ScriptContext& cx,
only_xml = false;
// copy random map settings (before entity creation)
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return LoadRMSettings();
}, L"CMapReader::LoadRMSettings", 50);
co_return mapReader->LoadRMSettings();
}, this), L"CMapReader::LoadRMSettings", 50);
// load player settings script (must be done before reading map)
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return LoadPlayerSettings();
}, L"CMapReader::LoadPlayerSettings", 50);
co_return mapReader->LoadPlayerSettings();
}, this), L"CMapReader::LoadPlayerSettings", 50);
// load map generator with random map script
PS::Loader::Register([this, scriptFile]
{
return StartMapGeneration(scriptFile);
}, L"CMapReader::StartMapGeneration", 1);
PS::Loader::Register([this]
{
return PollMapGeneration();
}, L"CMapReader::PollMapGeneration", 19999);
return RunMapGeneration(scriptFile);
}, L"CMapReader::RunMapGeneration", 20000);
// parse RMS results into terrain structure
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return ParseTerrain();
}, L"CMapReader::ParseTerrain", 500);
co_return mapReader->ParseTerrain();
}, this), L"CMapReader::ParseTerrain", 500);
// parse RMS results into environment settings
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return ParseEnvironment();
}, L"CMapReader::ParseEnvironment", 5);
co_return mapReader->ParseEnvironment();
}, this), L"CMapReader::ParseEnvironment", 5);
// parse RMS results into camera settings
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return ParseCamera();
}, L"CMapReader::ParseCamera", 5);
co_return mapReader->ParseCamera();
}, this), L"CMapReader::ParseCamera", 5);
// apply terrain data to the world
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return ApplyTerrainData();
}, L"CMapReader::ApplyTerrainData", 5);
co_return mapReader->ApplyTerrainData();
}, this), L"CMapReader::ApplyTerrainData", 5);
// parse RMS results into entities
PS::Loader::Register([this]
{
return StartParseEntities();
}, L"CMapReader::StartParseEntities", 10);
PS::Loader::Register([this]
{
return PollParseEntities();
}, L"CMapReader::PollParseEntities", 1000);
return ParseEntities();
}, L"CMapReader::ParseEntities", 1010);
// apply misc data to the world
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return ApplyData();
}, L"CMapReader::ApplyData", 5);
co_return mapReader->ApplyData();
}, this), L"CMapReader::ApplyData", 5);
// load map settings script (must be done after reading map)
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
return LoadMapSettings();
}, L"CMapReader::LoadMapSettings", 5);
co_return mapReader->LoadMapSettings();
}, this), L"CMapReader::LoadMapSettings", 5);
}
// UnpackTerrain: unpack the terrain from the end of the input data stream
// - data: map size, heightmap, list of textures used by map, texture tile assignments
int CMapReader::UnpackTerrain()
PS::Loader::Task CMapReader::UnpackTerrain()
{
// yield after this time is reached. balances increased progress bar
// smoothness vs. slowing down loading.
const double end_time = timer_Time() + 200e-3;
// first call to generator (this is skipped after first call,
// i.e. when the loop below was interrupted)
if (cur_terrain_tex == 0)
{
m_PatchesPerSide = (ssize_t)unpacker.UnpackSize();
@ -319,15 +304,15 @@ int CMapReader::UnpackTerrain()
size_t verticesPerSide = m_PatchesPerSide*PATCH_SIZE+1;
m_Heightmap.resize(SQR(verticesPerSide));
unpacker.UnpackRaw(&m_Heightmap[0], SQR(verticesPerSide)*sizeof(u16));
// unpack # textures
num_terrain_tex = unpacker.UnpackSize();
m_TerrainTextures.reserve(num_terrain_tex);
}
// unpack # textures
const std::size_t numTerrainTex{unpacker.UnpackSize()};
m_TerrainTextures.reserve(numTerrainTex);
// unpack texture names; find handle for each texture.
// interruptible.
while (cur_terrain_tex < num_terrain_tex)
for (std::size_t curTerrainTex{0}; curTerrainTex != numTerrainTex; ++curTerrainTex)
{
CStr texturename;
unpacker.UnpackString(texturename);
@ -338,8 +323,7 @@ int CMapReader::UnpackTerrain()
m_TerrainTextures.push_back(texentry);
}
cur_terrain_tex++;
LDR_CHECK_TIMEOUT(cur_terrain_tex, num_terrain_tex);
co_yield 100 * (curTerrainTex + 1) / numTerrainTex;
}
// unpack tile data [3ms]
@ -347,10 +331,7 @@ int CMapReader::UnpackTerrain()
m_Tiles.resize(size_t(SQR(tilesPerSide)));
unpacker.UnpackRaw(&m_Tiles[0], sizeof(STileDesc)*m_Tiles.size());
// reset generator state.
cur_terrain_tex = 0;
return 0;
co_return 0;
}
int CMapReader::ApplyTerrainData()
@ -504,7 +485,7 @@ public:
void ReadXML();
// return semantics: see Loader.cpp!LoadFunc.
int ProgressiveReadEntities();
PS::Loader::Task ProgressiveReadEntities();
private:
CXeromyces xmb_file;
@ -529,10 +510,6 @@ private:
XMBElementList nodes; // children of root
// loop counters
size_t node_idx;
size_t entity_idx;
// # entities+nonentities processed and total (for progress calc)
int completed_jobs, total_jobs;
@ -546,15 +523,12 @@ private:
void ReadCamera(XMBElement parent);
void ReadPaths(XMBElement parent);
void ReadTriggers(XMBElement parent);
int ReadEntities(XMBElement parent, double end_time);
PS::Loader::Task ReadEntities(XMBElement parent);
};
void CXMLReader::Init(const VfsPath& xml_filename)
{
// must only assign once, so do it here
node_idx = entity_idx = 0;
if (xmb_file.Load(g_VFS, xml_filename, "scenario") != PSRETURN_OK)
throw PSERROR_Game_World_MapLoadFailed("Could not read map XML file!");
@ -1023,7 +997,7 @@ void CXMLReader::ReadTriggers(XMBElement /*parent*/)
{
}
int CXMLReader::ReadEntities(XMBElement parent, double end_time)
PS::Loader::Task CXMLReader::ReadEntities(XMBElement parent)
{
XMBElementList entities = parent.GetChildNodes();
@ -1031,12 +1005,8 @@ int CXMLReader::ReadEntities(XMBElement parent, double end_time)
CSimulation2& sim = *m_MapReader.pSimulation2;
CmpPtr<ICmpPlayerManager> cmpPlayerManager(sim, SYSTEM_ENTITY);
while (entity_idx < entities.size())
for (XMBElement entity : entities)
{
// all new state at this scope and below doesn't need to be
// wrapped, since we only yield after a complete iteration.
XMBElement entity = entities[entity_idx++];
ENSURE(entity.GetNodeName() == el_entity);
XMBAttributeList attrs = entity.GetAttributes();
@ -1143,7 +1113,7 @@ int CXMLReader::ReadEntities(XMBElement parent, double end_time)
if (cmpPlayer && cmpPlayer->IsRemoved())
{
completed_jobs++;
LDR_CHECK_TIMEOUT(completed_jobs, total_jobs);
co_yield 100 * completed_jobs / total_jobs;
continue;
}
@ -1214,10 +1184,10 @@ int CXMLReader::ReadEntities(XMBElement parent, double end_time)
}
completed_jobs++;
LDR_CHECK_TIMEOUT(completed_jobs, total_jobs);
co_yield 100 * completed_jobs / total_jobs;
}
return 0;
co_return 0;
}
void CXMLReader::ReadXML()
@ -1266,32 +1236,28 @@ void CXMLReader::ReadXML()
}
}
int CXMLReader::ProgressiveReadEntities()
PS::Loader::Task CXMLReader::ProgressiveReadEntities()
{
// yield after this time is reached. balances increased progress bar
// smoothness vs. slowing down loading.
const double end_time = timer_Time() + 200e-3;
if (m_MapReader.m_SkipEntities)
co_return 0;
int ret;
while (node_idx < nodes.size())
for (XMBElement node : nodes)
{
XMBElement node = nodes[node_idx];
CStr name = xmb_file.GetElementString(node.GetNodeName());
if (name == "Entities")
{
if (!m_MapReader.m_SkipEntities)
{
ret = ReadEntities(node, end_time);
if (ret != 0) // error or timed out
return ret;
}
}
if (name != "Entities")
continue;
node_idx++;
PS::Loader::Task subTask{ReadEntities(node)};
while (!subTask.IsDone())
{
co_yield subTask.GetProgress();
subTask.Step(-1);
}
if (subTask.Get() < 0)
co_return subTask.Get();
}
return 0;
co_return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -1339,17 +1305,22 @@ int CMapReader::ReadXML()
}
// progressive
int CMapReader::ReadXMLEntities()
PS::Loader::Task CMapReader::ReadXMLEntities()
{
if (!m_XmlReader)
m_XmlReader = std::make_unique<CXMLReader>(m_FilenameXml, *this);
int ret = m_XmlReader->ProgressiveReadEntities();
// finished or failed
if (ret <= 0)
m_XmlReader.reset();
PS::Loader::Task task{m_XmlReader->ProgressiveReadEntities()};
while (!task.IsDone())
{
co_yield task.GetProgress();
// Do as litle as possible.
task.Step(-1);
}
return ret;
m_XmlReader.reset();
co_return task.Get();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -1366,22 +1337,20 @@ int CMapReader::LoadRMSettings()
return 0;
}
struct CMapReader::GeneratorState
[[noreturn]] void ThrowMapGenerationError()
{
throw PSERROR_Game_World_MapLoadFailed{
"Error generating random map.\nCheck application log for details."};
}
PS::Loader::Task CMapReader::RunMapGeneration(const CStrW& scriptFile)
{
std::atomic<int> progress{1};
Future<Script::StructuredClone> task;
};
int CMapReader::StartMapGeneration(const CStrW& scriptFile)
{
ScriptRequest rq(pSimulation2->GetScriptInterface());
m_GeneratorState = std::make_unique<GeneratorState>();
// The settings are stringified to pass them to the task.
m_GeneratorState->task = {g_TaskManager,
[&progress = m_GeneratorState->progress, scriptFile,
settings = Script::StringifyJSON(rq, &m_ScriptSettings)](const StopToken stopToken)
Future<Script::StructuredClone> task = {g_TaskManager,
[&progress, scriptFile, settings = Script::StringifyJSON(ScriptRequest{
pSimulation2->GetScriptInterface()}, &m_ScriptSettings)](const StopToken stopToken)
{
PROFILE2("Map Generation");
@ -1400,29 +1369,19 @@ int CMapReader::StartMapGeneration(const CStrW& scriptFile)
return RunMapGenerationScript(stopToken, progress, mapgenInterface, scriptPath, settings);
}};
return 0;
}
[[noreturn]] void ThrowMapGenerationError()
{
throw PSERROR_Game_World_MapLoadFailed{
"Error generating random map.\nCheck application log for details."};
}
int CMapReader::PollMapGeneration()
{
ENSURE(m_GeneratorState);
if (IsQuitRequested())
while (!task.IsDone())
{
LOGWARNING("Quit requested!");
return -1;
co_yield progress.load();
co_await std::suspend_always{};
if (IsQuitRequested())
{
LOGWARNING("Quit requested!");
co_return -1;
}
}
if (!m_GeneratorState->task.IsDone())
return m_GeneratorState->progress.load();
const Script::StructuredClone results{m_GeneratorState->task.Get()};
const Script::StructuredClone results{task.Get()};
if (!results)
ThrowMapGenerationError();
@ -1436,7 +1395,7 @@ int CMapReader::PollMapGeneration()
m_MapData.init(rq.cx, data);
return 0;
co_return 0;
};
@ -1468,17 +1427,14 @@ int CMapReader::ParseTerrain()
// load textures
std::vector<std::string> textureNames;
getTerrainProperty(m_MapData, "textureNames", textureNames);
num_terrain_tex = textureNames.size();
while (cur_terrain_tex < num_terrain_tex)
for (const std::string& textureName : textureNames)
{
if (CTerrainTextureManager::IsInitialised())
{
CTerrainTextureEntry* texentry = g_TexMan.FindTexture(textureNames[cur_terrain_tex]);
CTerrainTextureEntry* texentry = g_TexMan.FindTexture(textureName);
m_TerrainTextures.push_back(texentry);
}
cur_terrain_tex++;
}
// build tile data
@ -1514,12 +1470,10 @@ int CMapReader::ParseTerrain()
}
}
// reset generator state
cur_terrain_tex = 0;
return 0;
}
struct CMapReader::ParseEntitiesState
struct ParseEntitiesState
{
ScriptRequest rq;
CmpPtr<ICmpPlayerManager> cmpPlayerManager;
@ -1530,28 +1484,25 @@ struct CMapReader::ParseEntitiesState
: rq(sim.GetScriptInterface()), cmpPlayerManager(sim, SYSTEM_ENTITY) {}
};
int CMapReader::StartParseEntities()
PS::Loader::Task CMapReader::ParseEntities()
{
PROFILE2("StartParseEntities");
PROFILE2("ParseEntities");
m_ParseEntitiesState = std::make_unique<ParseEntitiesState>(*pSimulation2);
if (!Script::GetProperty(m_ParseEntitiesState->rq, m_MapData, "entities", m_ParseEntitiesState->entities))
LOGWARNING("CMapReader::ParseEntities() failed to get 'entities' property");
return 0;
}
int CMapReader::PollParseEntities()
{
PROFILE2("PollParseEntities");
ParseEntitiesState& state{*m_ParseEntitiesState};
CSimulation2& sim{*pSimulation2};
const size_t numberOfEntitesToLoadPerCall{100};
for (size_t iteration{0}; iteration < numberOfEntitesToLoadPerCall && state.currentEntityIndex < state.entities.size(); ++iteration, ++state.currentEntityIndex)
ScriptRequest rq{sim.GetScriptInterface()};
CmpPtr<ICmpPlayerManager> cmpPlayerManager{sim, SYSTEM_ENTITY};
std::vector<Entity> entities;
if (!Script::GetProperty(rq, m_MapData, "entities", entities))
LOGWARNING("CMapReader::ParseEntities() failed to get 'entities' property");
for (std::size_t index{0}; index != entities.size(); ++index)
{
const Entity& currEnt{state.entities[state.currentEntityIndex]};
co_yield Clamp<int>(index * 80 / entities.size(), 20, 100);
const Entity& currEnt{entities[index]};
// Get current entity struct
entity_id_t player = state.cmpPlayerManager->GetPlayerByID(currEnt.playerID);
entity_id_t player = cmpPlayerManager->GetPlayerByID(currEnt.playerID);
CmpPtr<ICmpPlayer> cmpPlayer(sim, player);
// Don't add entities for removed players.
if (cmpPlayer && cmpPlayer->IsRemoved())
@ -1591,12 +1542,7 @@ int CMapReader::PollParseEntities()
}
}
if (state.currentEntityIndex < state.entities.size())
return Clamp<int>(state.currentEntityIndex * 80 / state.entities.size(), 20, 100);
m_ParseEntitiesState.reset();
return 0;
co_return 0;
}
int CMapReader::ParseEnvironment()

View file

@ -28,6 +28,7 @@
#include "ps/CStr.h"
#include "ps/Errors.h"
#include "ps/FileIo.h"
#include "ps/Loader.h"
#include "simulation2/system/Entity.h"
#include <cstddef>
@ -82,7 +83,7 @@ private:
int LoadMapSettings();
// UnpackTerrain: unpack the terrain from the input stream
int UnpackTerrain();
PS::Loader::Task UnpackTerrain();
// UnpackCinema: unpack the cinematic tracks from the input stream
int UnpackCinema();
@ -94,21 +95,19 @@ private:
int ReadXML();
// read entity data from the XML file
int ReadXMLEntities();
PS::Loader::Task ReadXMLEntities();
// Copy random map settings over to sim
int LoadRMSettings();
// Generate random map
int StartMapGeneration(const CStrW& scriptFile);
int PollMapGeneration();
PS::Loader::Task RunMapGeneration(const CStrW& scriptFile);
// Parse script data into terrain
int ParseTerrain();
// Parse script data into entities
int StartParseEntities();
int PollParseEntities();
PS::Loader::Task ParseEntities();
// Parse script data into environment
int ParseEnvironment();
@ -134,12 +133,6 @@ private:
JS::PersistentRootedValue m_ScriptSettings;
JS::PersistentRootedValue m_MapData;
struct GeneratorState;
std::unique_ptr<GeneratorState> m_GeneratorState;
struct ParseEntitiesState;
std::unique_ptr<ParseEntitiesState> m_ParseEntitiesState;
CFileUnpacker unpacker;
CTerrain* pTerrain;
WaterManager* pWaterMan;
@ -159,11 +152,6 @@ private:
entity_id_t m_StartingCameraTarget;
CVector3D m_StartingCamera;
// UnpackTerrain generator state
// It's important to initialize it to 0 - resets generator state
size_t cur_terrain_tex{0};
size_t num_terrain_tex;
std::unique_ptr<CXMLReader> m_XmlReader;
};

View file

@ -122,41 +122,24 @@ static Status AddTextureCallback(const VfsPath& pathname, const CFileInfo&, cons
return INFO::OK;
}
struct CTerrainTextureManager::LoadTexturesState
PS::Loader::Task CTerrainTextureManager::LoadTerrainTextures()
{
vfs::ForEachFileContext context;
AddTextureCallbackData data;
LoadTexturesState(const VfsPath& startPath, CTerrainTextureManager* self)
: context{startPath}, data{self, std::make_shared<CTerrainProperties>(CTerrainPropertiesPtr())} {}
};
int CTerrainTextureManager::StartTerrainTextures()
{
m_LoadTexturesState = std::make_unique<LoadTexturesState>(VfsPath{L"art/terrains/"}, this);
return 0;
}
int CTerrainTextureManager::PollTerrainTextures()
{
LoadTexturesState& state{*m_LoadTexturesState};
const size_t numberOfDirectoriesToLoadPerCall{10};
for (size_t iteration{0}; !state.context.empty() && iteration < numberOfDirectoriesToLoadPerCall; ++iteration)
vfs::ForEachFileContext context{VfsPath{L"art/terrains/"}};
AddTextureCallbackData data{this, std::make_shared<CTerrainProperties>(CTerrainPropertiesPtr())};
while (true)
{
vfs::ForEachFileNext(state.context, g_VFS, AddTextureCallback, (uintptr_t)&state.data, L"*.xml", vfs::DIR_RECURSIVE, AddTextureDirCallback, (uintptr_t)&state.data);
}
vfs::ForEachFileNext(context, g_VFS, AddTextureCallback,
reinterpret_cast<uintptr_t>(&data), L"*.xml", vfs::DIR_RECURSIVE,
AddTextureDirCallback, reinterpret_cast<uintptr_t>(&data));
if (context.empty())
co_return 0;
if (!state.context.empty())
{
// We don't know exact number so just using a rough approximation of the
// current number.
const size_t totalApproximateAmountOfTextures{1000};
return Clamp<int>(m_TextureEntries.size() * 90 / totalApproximateAmountOfTextures, 10, 100);
co_yield Clamp<int>(m_TextureEntries.size() * 90 / totalApproximateAmountOfTextures, 10, 100);
}
m_LoadTexturesState.reset();
return 0;
}
CTerrainGroup* CTerrainTextureManager::FindGroup(const CStr& name)

View file

@ -21,6 +21,7 @@
#include "lib/file/vfs/vfs_path.h"
#include "lib/types.h"
#include "ps/CStr.h"
#include "ps/Loader.h"
#include "ps/Singleton.h"
#include "renderer/backend/ITexture.h"
@ -99,8 +100,7 @@ public:
// Find all XML's in the directory (with subdirs) and try to load them as
// terrain XML's
int StartTerrainTextures();
int PollTerrainTextures();
PS::Loader::Task LoadTerrainTextures();
void UnloadTerrainTextures();
@ -137,9 +137,6 @@ private:
// A way to separate file loading and uploading to GPU to not stall uploading.
// Once we get a properly threaded loading we might optimize that.
std::vector<CTerrainTextureManager::TerrainAlphaMap::iterator> m_AlphaMapsToUpload;
struct LoadTexturesState;
std::unique_ptr<LoadTexturesState> m_LoadTexturesState;
};
// access to sole CTerrainTextureManager object

View file

@ -282,22 +282,23 @@ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& saved
m_World->RegisterInit(mapFile, scriptInterface.GetContext(), settings, m_PlayerID);
}
if (m_GameView)
PS::Loader::Register([&waterManager = g_Renderer.GetSceneRenderer().GetWaterManager()]
PS::Loader::Register([]() -> PS::Loader::Task
{
return waterManager.LoadWaterTextures();
co_return g_Renderer.GetSceneRenderer().GetWaterManager().LoadWaterTextures();
}, L"LoadWaterTextures", 80);
if (m_IsSavedGame)
PS::Loader::Register([this, savedState]
PS::Loader::Register(std::bind_front(
[](CGame* game, const std::string& state) -> PS::Loader::Task
{
return LoadInitialState(savedState);
}, L"Loading game", 1000);
co_return game->LoadInitialState(state);
}, this, savedState), L"Loading game", 1000);
if (m_IsVisualReplay)
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CGame* game) -> PS::Loader::Task
{
return LoadVisualReplayData();
}, L"Loading visual replay data", 1000);
co_return game->LoadVisualReplayData();
}, this), L"Loading visual replay data", 1000);
PS::Loader::EndRegistering();
}

View file

@ -24,11 +24,11 @@
#include "lib/code_annotation.h"
#include "lib/secure_crt.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include <deque>
#include <numeric>
#include <optional>
#include <string>
#include <utility>
@ -84,13 +84,7 @@ struct LoadRequest
using LoadRequests = std::deque<LoadRequest>;
LoadRequests load_requests;
// Returns true if the return code indicates that the `LoadRequest` didn't
// finish and should be reinvoked in the next frame.
bool WasInterrupted(const int ret)
{
return 0 < ret && ret <= 100;
}
std::optional<PS::Loader::Task> currentTask;
} // anonymous namespace
// call before starting to register load requests.
@ -118,7 +112,7 @@ void Register(LoadFunc func, std::wstring description, int estimatedDurationMs)
{
ENSURE(state == REGISTERING); // must be called between PS::Loader::(Begin|End)Register
load_requests.emplace_back(std::move(func), std::move(description), estimatedDurationMs);
load_requests.push_back({std::move(func), std::move(description), estimatedDurationMs});
}
@ -208,8 +202,18 @@ ProgressiveLoadResult ProgressiveLoad(double time_budget)
// call this task's function and bill elapsed time.
const double t0 = timer_Time();
const int status = lr.func();
const bool timed_out = WasInterrupted(status);
if (!currentTask.has_value())
currentTask.emplace(lr.func());
try
{
currentTask->Step(time_left);
}
catch(...)
{
currentTask.reset();
throw;
}
const bool timed_out = !currentTask->IsDone();
const double elapsed_time = timer_Time() - t0;
time_left -= elapsed_time;
task_elapsed_time += elapsed_time;
@ -232,7 +236,7 @@ ProgressiveLoadResult ProgressiveLoad(double time_budget)
// note: monotonicity is guaranteed since we never add more than
// its estimated_duration_ms.
if(timed_out)
current_estimate += estimated_duration * status/100.0;
current_estimate += estimated_duration * currentTask->GetProgress() / 100.0;
progress = current_estimate / total_estimated_duration;
}
@ -248,19 +252,16 @@ ProgressiveLoadResult ProgressiveLoad(double time_budget)
// the next iteration of the main loop.
// rationale: bail immediately instead of remembering the first
// error that came up so we can report all errors that happen.
else if(status < 0)
{
ret.status = static_cast<Status>(status);
goto done;
}
else if(currentTask->Get() < 0)
ret.status = static_cast<Status>(currentTask->Get());
// .. function called PS::Loader::Cancel; abort. return OK since this is an
// intentional cancellation, not an error.
else if(state != LOADING)
{
ret.status = INFO::OK;
goto done;
}
// .. succeeded; continue and process next queued task.
currentTask.reset();
goto done;
}
// queue is empty, we just finished.

View file

@ -23,9 +23,13 @@
#include "lib/debug.h"
#include "lib/status.h"
#include "lib/timer.h"
#include <coroutine>
#include <exception>
#include <functional>
#include <string>
#include <utility>
namespace PS::Loader
{
@ -104,19 +108,122 @@ Then in the main loop, call PS::Loader::ProgressiveLoad().
// or issuing via console while already loading.
void BeginRegistering();
/**
* Coroutine which performs the actual work.
*
* `co_yield ...` can be used to yield the current progress. Iff the timeout is
* reached, the coroutine suspends.
* `co_await std::suspend_always{}` is usefull to force a suspention. e.g. When
* no progress can be made such as when the work is done on a different
* thread.
* `co_return 0` notifies the loader that the task is fineshed without an
* error.
* `co_return ...` when the returned value is negative the loader interprets
* that as a task failure. `PS::Loader::ProgressiveLoad` will abort
* immediately and forward the error code.
*/
class Task
{
public:
class promise_type
{
class SuspendIf
{
public:
explicit SuspendIf(const bool suspend) :
m_Suspend{suspend}
{}
// callback function of a task; performs the actual work.
//
// return semantics:
// - if the entire task was successfully completed, return 0;
// it will then be de-queued.
// - if the work can be split into smaller subtasks, process those until
// <time_left> is reached or exceeded and then return an estimate
// of progress in percent (<= 100, otherwise it's a warning;
// != 0, or it's treated as "finished")
// - on failure, return a negative error code or 'warning' (see above);
// PS::Loader::ProgressiveLoad will abort immediately and return that.
using LoadFunc = std::function<int()>;
bool await_ready() const noexcept
{
return !m_Suspend;
}
void await_suspend(std::coroutine_handle<promise_type>) const noexcept
{}
void await_resume() const noexcept
{}
private:
bool m_Suspend;
};
public:
Task get_return_object() noexcept
{
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() const noexcept { return {}; }
std::suspend_always final_suspend() const noexcept { return {}; }
void return_value(const int result) noexcept
{
m_Result = result;
}
void unhandled_exception() noexcept
{
m_Exception = std::current_exception();
}
SuspendIf yield_value(const int progress) noexcept
{
m_Progress = progress;
return SuspendIf{m_StepEnd < timer_Time()};
}
int m_Progress{0};
double m_StepEnd;
int m_Result{0};
std::exception_ptr m_Exception;
};
Task(const Task&) = delete;
Task& operator =(const Task&) = delete;
Task(Task&& other) noexcept :
m_Handle{std::exchange(other.m_Handle, {})}
{}
Task& operator =(Task&& other) noexcept
{
m_Handle = std::exchange(other.m_Handle, {});
return *this;
}
~Task()
{
if (m_Handle)
m_Handle.destroy();
}
[[nodiscard]] double GetProgress() const noexcept
{
return m_Handle.promise().m_Progress;
}
void Step(const double timeBudget)
{
m_Handle.promise().m_StepEnd = timeBudget + timer_Time();
m_Handle.resume();
std::exception_ptr exception{std::exchange(m_Handle.promise().m_Exception, {})};
if (exception)
std::rethrow_exception(std::move(exception));
}
[[nodiscard]] bool IsDone() const noexcept
{
return m_Handle.done();
}
[[nodiscard]] int Get() const noexcept
{
return m_Handle.promise().m_Result;
}
private:
explicit Task(std::coroutine_handle<promise_type> h) noexcept :
m_Handle{std::move(h)}
{}
std::coroutine_handle<promise_type> m_Handle;
};
using LoadFunc = std::function<PS::Loader::Task()>;
// register a task (later processed in FIFO order).
// <func>: function that will perform the actual work; see LoadFunc.
@ -169,22 +276,6 @@ ProgressiveLoadResult ProgressiveLoad(double time_budget);
// returns 0 on success or a negative error code.
Status NonprogressiveLoad();
// boilerplate check-if-timed-out and return-progress-percent code.
// completed_jobs and total_jobs are ints and must be updated by caller.
// assumes presence of a local variable (double)<end_time>
// (as returned by timer_Time()) that indicates the time at which to abort.
#define LDR_CHECK_TIMEOUT(completed_jobs, total_jobs)\
if(timer_Time() > end_time)\
{\
size_t progress_percent = ((completed_jobs)*100 / (total_jobs));\
/* 0 means "finished", so don't return that! */\
if(progress_percent == 0)\
progress_percent = 1;\
ENSURE(0 < progress_percent && progress_percent <= 100);\
return (int)progress_percent;\
}
} // namespace PS::Loader
#endif // #ifndef INCLUDED_LOADER

View file

@ -86,10 +86,10 @@ void CWorld::RegisterInit(const CStrW& mapFile, const ScriptContext& cx, JS::Han
m_Game.GetSimulation2(), &m_Game.GetSimulation2()->GetSimContext(), playerID,
false);
// fails immediately, or registers for delay loading
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CWorld* world) -> PS::Loader::Task
{
return DeleteMapReader();
}, L"CWorld::DeleteMapReader", 5);
co_return world->DeleteMapReader();
}, this), L"CWorld::DeleteMapReader", 5);
}
catch (PSERROR_File& err)
{
@ -111,10 +111,10 @@ void CWorld::RegisterInitRMS(const CStrW& scriptFile, const ScriptContext& cx, J
pTriggerManager, CRenderer::IsInitialised() ? &g_Renderer.GetPostprocManager() : nullptr,
m_Game.GetSimulation2(), playerID);
// registers for delay loading
PS::Loader::Register([this]
PS::Loader::Register(std::bind_front([](CWorld* world) -> PS::Loader::Task
{
return DeleteMapReader();
}, L"CWorld::DeleteMapReader", 5);
co_return world->DeleteMapReader();
}, this), L"CWorld::DeleteMapReader", 5);
}
int CWorld::DeleteMapReader()

View file

@ -0,0 +1,104 @@
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "lib/self_test.h"
#include "ps/Loader.h"
class TestLoader : public CxxTest::TestSuite
{
public:
void test_PausedYield()
{
int position{0};
PS::Loader::Task task{[](int& p) -> PS::Loader::Task
{
p = 1;
co_yield 50;
p = 2;
co_return 0;
}(position)};
TS_ASSERT_EQUALS(task.GetProgress(), 0);
TS_ASSERT(!task.IsDone());
TS_ASSERT_EQUALS(position, 0);
task.Step(-1);
TS_ASSERT_EQUALS(task.GetProgress(), 50);
TS_ASSERT(!task.IsDone());
TS_ASSERT_EQUALS(position, 1);
task.Step(-1);
TS_ASSERT_EQUALS(task.GetProgress(), 50);
TS_ASSERT(task.IsDone());
TS_ASSERT_EQUALS(position, 2);
}
void test_UnpausedYield()
{
int position{0};
PS::Loader::Task task{[](int& p) -> PS::Loader::Task
{
p = 1;
co_yield 50;
p = 2;
co_return 0;
}(position)};
TS_ASSERT_EQUALS(task.GetProgress(), 0);
TS_ASSERT(!task.IsDone());
TS_ASSERT_EQUALS(position, 0);
task.Step(10);
TS_ASSERT_EQUALS(task.GetProgress(), 50);
TS_ASSERT(task.IsDone());
TS_ASSERT_EQUALS(position, 2);
}
void test_ForceAwait()
{
int position{0};
PS::Loader::Task task{[](int& p) -> PS::Loader::Task
{
p = 1;
co_yield 50;
p = 2;
co_await std::suspend_always{};
p = 3;
co_return 0;
}(position)};
TS_ASSERT_EQUALS(task.GetProgress(), 0);
TS_ASSERT(!task.IsDone());
TS_ASSERT_EQUALS(position, 0);
task.Step(10);
TS_ASSERT_EQUALS(task.GetProgress(), 50);
TS_ASSERT(!task.IsDone());
TS_ASSERT_EQUALS(position, 2);
task.Step(-1);
TS_ASSERT_EQUALS(task.GetProgress(), 50);
TS_ASSERT(task.IsDone());
TS_ASSERT_EQUALS(position, 3);
}
};

View file

@ -132,7 +132,7 @@ public:
return static_cast<CSimulation2Impl*>(param)->ReloadChangedFile(path);
}
int ProgressiveLoad();
PS::Loader::Task ProgressiveLoad();
void Update(int turnLength, const std::vector<SimulationCommand>& commands);
static void UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector<SimulationCommand>& commands);
void Interpolate(float simFrameLength, float frameOffset, float realFrameLength);
@ -267,15 +267,9 @@ Status CSimulation2Impl::ReloadChangedFile(const VfsPath& path)
return INFO::OK;
}
int CSimulation2Impl::ProgressiveLoad()
PS::Loader::Task CSimulation2Impl::ProgressiveLoad()
{
// yield after this time is reached. balances increased progress bar
// smoothness vs. slowing down loading.
const double end_time = timer_Time() + 200e-3;
int ret;
do
while (true)
{
bool progressed = false;
int total = 0;
@ -286,13 +280,10 @@ int CSimulation2Impl::ProgressiveLoad()
m_ComponentManager.BroadcastMessage(msg);
if (!progressed || total == 0)
return 0; // we have nothing left to load
co_return 0; // we have nothing left to load
ret = Clamp(100*progress / total, 1, 100);
co_yield Clamp(100*progress / total, 1, 100);
}
while (timer_Time() < end_time);
return ret;
}
void CSimulation2Impl::DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix)
@ -849,7 +840,7 @@ void CSimulation2::LoadMapSettings()
m->LoadTriggerScripts(m->m_ComponentManager, m->m_MapSettings, &m->m_LoadedScripts);
}
int CSimulation2::ProgressiveLoad()
PS::Loader::Task CSimulation2::ProgressiveLoad()
{
return m->ProgressiveLoad();
}

View file

@ -21,6 +21,7 @@
#include "lib/code_annotation.h"
#include "lib/file/vfs/vfs_path.h"
#include "lib/status.h"
#include "ps/Loader.h"
#include "simulation2/system/DebugOptions.h"
#include "simulation2/system/Entity.h"
@ -140,7 +141,7 @@ public:
/**
* RegMemFun incremental loader function.
*/
int ProgressiveLoad();
PS::Loader::Task ProgressiveLoad();
/**
* Reload any scripts that were loaded from the given filename.