mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-08-01 18:29:24 -04:00
perf(wmo): upload model groups incrementally to kill the streaming hitch
Terrain streaming stalled ~165ms finalising a tile, and the phase timing put essentially all of it in one WMO_MODELS step: a single model taking 158ms against an 8ms budget. Within that load, group resource creation was 81% of the time — 131ms for 286 groups — with textures at 13% and merged batches at 6%, so the groups are what had to be split. loadModelIncremental uploads a bounded slice of groups per call and returns InProgress while more remain. The accumulating model lives in loadingModels_ until every group is up, so a half-built model is never visible to rendering, and its textures and materials are done once and skipped on resumed calls. loadModel keeps its old all-at-once behaviour for callers outside terrain streaming by looping until completion. The result is explicit rather than a bare bool, because the caller has to tell "more groups to come" from "this model failed" — overloading false would have spun forever on a bad model. Terrain finalisation now takes one model per step with a 6ms group budget, and no longer uploads two models per step when the loader threads are idle, which had doubled the worst case for nothing.
This commit is contained in:
@@ -71,6 +71,14 @@ public:
|
||||
*/
|
||||
bool loadModel(const pipeline::WMOModel& model, uint32_t id);
|
||||
|
||||
enum class ModelLoadResult { Complete, InProgress, Failed };
|
||||
|
||||
/// Upload a model a few groups at a time. InProgress means call again with
|
||||
/// the same arguments; a single large model can otherwise take over a
|
||||
/// hundred milliseconds, which lands as a visible hitch when terrain
|
||||
/// streaming finalises a tile.
|
||||
ModelLoadResult loadModelIncremental(const pipeline::WMOModel& model, uint32_t id, float budgetMs);
|
||||
|
||||
/**
|
||||
* Check if a WMO model is currently resident in the renderer
|
||||
* @param id WMO model identifier
|
||||
@@ -549,6 +557,14 @@ private:
|
||||
// For each group: which portal refs belong to it (start index, count)
|
||||
std::vector<std::pair<uint16_t, uint16_t>> groupPortalRefs;
|
||||
|
||||
// Set once the textures and materials below have been populated, so a
|
||||
// resumed load skips straight to the groups it has left.
|
||||
bool setupDone = false;
|
||||
// Next group to upload. A large model's groups are spread across several
|
||||
// calls under a time budget rather than uploaded in one stall.
|
||||
size_t nextGroupIndex = 0;
|
||||
uint32_t loadedGroups = 0;
|
||||
|
||||
uint32_t getTotalTriangles() const {
|
||||
uint32_t total = 0;
|
||||
for (const auto& group : groups) {
|
||||
@@ -729,6 +745,9 @@ private:
|
||||
|
||||
// Loaded models (modelId -> ModelData)
|
||||
std::unordered_map<uint32_t, ModelData> loadedModels;
|
||||
// Models part-way through an incremental load; moved into loadedModels once
|
||||
// every group is uploaded, so a half-built model is never rendered.
|
||||
std::unordered_map<uint32_t, ModelData> loadingModels_;
|
||||
size_t modelCacheLimit_ = 4000;
|
||||
uint32_t modelLimitRejectWarnings_ = 0;
|
||||
|
||||
|
||||
@@ -1176,22 +1176,24 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
||||
&pending->preloadedWMONormalMapVariances);
|
||||
wmoRenderer->setDeferNormalMaps(true);
|
||||
|
||||
bool wmoWorkersIdle;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(queueMutex);
|
||||
wmoWorkersIdle = loadQueue.empty() && readyQueue.empty();
|
||||
}
|
||||
const size_t kWmosPerStep = wmoWorkersIdle ? 2 : 1;
|
||||
size_t uploaded = 0;
|
||||
while (ft.wmoModelIndex < pending->wmoModels.size() && uploaded < kWmosPerStep) {
|
||||
// One model per step, and a large model spread across several
|
||||
// steps: a 286-group WMO took 131ms to upload in one go, against
|
||||
// this phase's 8ms budget. Uploading two per step when the workers
|
||||
// were idle only doubled that worst case.
|
||||
constexpr float kWmoGroupBudgetMs = 6.0f;
|
||||
while (ft.wmoModelIndex < pending->wmoModels.size()) {
|
||||
auto& wmoReady = pending->wmoModels[ft.wmoModelIndex];
|
||||
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
|
||||
ft.wmoModelIndex++;
|
||||
} else {
|
||||
wmoRenderer->loadModel(wmoReady.model, wmoReady.modelId);
|
||||
ft.wmoModelIndex++;
|
||||
uploaded++;
|
||||
continue;
|
||||
}
|
||||
const auto result = wmoRenderer->loadModelIncremental(
|
||||
wmoReady.model, wmoReady.modelId, kWmoGroupBudgetMs);
|
||||
if (result == WMORenderer::ModelLoadResult::InProgress) {
|
||||
break; // same model resumes on the next call
|
||||
}
|
||||
ft.wmoModelIndex++; // Complete or Failed — either way, move on
|
||||
break; // one model per step
|
||||
}
|
||||
wmoRenderer->setDeferNormalMaps(false);
|
||||
wmoRenderer->setPredecodedBLPCache(nullptr);
|
||||
|
||||
@@ -353,9 +353,20 @@ void WMORenderer::shutdown() {
|
||||
}
|
||||
|
||||
bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||
// No budget: run to completion, which is what callers outside terrain
|
||||
// streaming expect.
|
||||
for (;;) {
|
||||
const ModelLoadResult r = loadModelIncremental(model, id, 0.0f);
|
||||
if (r == ModelLoadResult::InProgress) continue;
|
||||
return r == ModelLoadResult::Complete;
|
||||
}
|
||||
}
|
||||
|
||||
WMORenderer::ModelLoadResult WMORenderer::loadModelIncremental(
|
||||
const pipeline::WMOModel& model, uint32_t id, float budgetMs) {
|
||||
if (!model.isValid()) {
|
||||
core::Logger::getInstance().error("Cannot load invalid WMO model");
|
||||
return false;
|
||||
return ModelLoadResult::Failed;
|
||||
}
|
||||
|
||||
// Check if already loaded
|
||||
@@ -392,10 +403,10 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||
" has only fallback textures; forcing one-time reload");
|
||||
unloadModel(id);
|
||||
} else {
|
||||
return true;
|
||||
return ModelLoadResult::Complete;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
return ModelLoadResult::Complete;
|
||||
}
|
||||
}
|
||||
if (loadedModels.size() >= modelCacheLimit_) {
|
||||
@@ -405,13 +416,15 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||
"), skipping model load: id=", id);
|
||||
}
|
||||
++modelLimitRejectWarnings_;
|
||||
return false;
|
||||
return ModelLoadResult::Failed;
|
||||
}
|
||||
|
||||
core::Logger::getInstance().debug("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
|
||||
model.textures.size(), " textures...");
|
||||
|
||||
ModelData modelData;
|
||||
// The accumulating model lives across calls, so a resumed load picks up its
|
||||
// textures, materials and the groups already uploaded.
|
||||
ModelData& modelData = loadingModels_[id];
|
||||
modelData.id = id;
|
||||
modelData.boundingBoxMin = model.boundingBoxMin;
|
||||
modelData.boundingBoxMax = model.boundingBoxMax;
|
||||
@@ -436,11 +449,12 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||
// submission with one fence wait, instead of one per upload.
|
||||
vkCtx_->beginUploadBatch();
|
||||
|
||||
// Which of the three per-model passes owns the time. One of these steps was
|
||||
// measured at 158ms, stalling terrain streaming; splitting the wrong one
|
||||
// would not help.
|
||||
const auto wmoLoadT0 = std::chrono::steady_clock::now();
|
||||
|
||||
// Textures and materials are model-level and done once; a resumed call has
|
||||
// them already and goes straight to the remaining groups.
|
||||
if (!modelData.setupDone) {
|
||||
|
||||
// Load textures for this model
|
||||
core::Logger::getInstance().debug(" WMO has ", model.textures.size(), " texture paths, ", model.materials.size(), " materials");
|
||||
if (assetManager && !model.textures.empty()) {
|
||||
@@ -511,6 +525,12 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
modelData.setupDone = true;
|
||||
} // end one-time setup
|
||||
|
||||
// Reads only the model, so it is rebuilt cheaply on every resumed call
|
||||
// rather than kept alive across them.
|
||||
// Helper: look up group name from MOGN raw data via MOGI nameOffset
|
||||
auto getGroupName = [&](uint32_t groupIdx) -> std::string {
|
||||
if (groupIdx < model.groupInfo.size()) {
|
||||
@@ -526,9 +546,20 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||
|
||||
const auto wmoLoadT1 = std::chrono::steady_clock::now();
|
||||
|
||||
// Create GPU resources for each group
|
||||
uint32_t loadedGroups = 0;
|
||||
for (size_t gi = 0; gi < model.groups.size(); gi++) {
|
||||
// Create GPU resources for each group, a bounded number per call. A model
|
||||
// with hundreds of groups would otherwise upload them all in one step: the
|
||||
// worst measured was 286 groups at 131ms, against an 8ms budget.
|
||||
const auto groupStart = std::chrono::steady_clock::now();
|
||||
for (size_t gi = modelData.nextGroupIndex; gi < model.groups.size(); gi++) {
|
||||
modelData.nextGroupIndex = gi + 1;
|
||||
if (budgetMs > 0.0f) {
|
||||
const float spent = std::chrono::duration<float, std::milli>(
|
||||
std::chrono::steady_clock::now() - groupStart).count();
|
||||
if (spent >= budgetMs && gi + 1 < model.groups.size()) {
|
||||
vkCtx_->endUploadBatch();
|
||||
return ModelLoadResult::InProgress; // resume here next call
|
||||
}
|
||||
}
|
||||
const auto& wmoGroup = model.groups[gi];
|
||||
// Skip empty groups
|
||||
if (wmoGroup.vertices.empty() || wmoGroup.indices.empty()) {
|
||||
@@ -568,13 +599,15 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||
resources.isLOD = true;
|
||||
}
|
||||
modelData.groups.push_back(resources);
|
||||
loadedGroups++;
|
||||
modelData.loadedGroups++;
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedGroups == 0) {
|
||||
if (modelData.loadedGroups == 0) {
|
||||
core::Logger::getInstance().warning("No valid groups loaded for WMO ", id);
|
||||
return false;
|
||||
vkCtx_->endUploadBatch();
|
||||
loadingModels_.erase(id);
|
||||
return ModelLoadResult::Failed;
|
||||
}
|
||||
|
||||
const auto wmoLoadT2 = std::chrono::steady_clock::now();
|
||||
@@ -913,9 +946,11 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||
}
|
||||
}
|
||||
|
||||
modelData.setupDone = true;
|
||||
loadedModels[id] = std::move(modelData);
|
||||
core::Logger::getInstance().debug("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
|
||||
return true;
|
||||
loadingModels_.erase(id);
|
||||
core::Logger::getInstance().debug("WMO model ", id, " loaded successfully (", modelData.loadedGroups, " groups)");
|
||||
return ModelLoadResult::Complete;
|
||||
}
|
||||
|
||||
bool WMORenderer::isModelLoaded(uint32_t id) const {
|
||||
|
||||
Reference in New Issue
Block a user