From 2051eeed429fa65ae7c69fdfbfb0c8c48b28302d Mon Sep 17 00:00:00 2001 From: Leonardo Zide Date: Tue, 13 Oct 2026 19:32:12 -0700 Subject: [PATCH] Simplify asset loader lifecycle and paused scheduling Make the loading lifecycle easier to follow by ordering functions by phase, splitting completion handling by asset type, and sharing color and texture preparation. Use Piece terminology, rename model rebuilding, and document reference and waiting behavior. Keep pending work in the normal queues while paused, refresh piece snapshots before resuming, and remove duplicate enqueue and cancellation paths. QueuePiece now explicitly schedules background work. Validated with a Debug build, 14 CLI export checks, and a temporary lifecycle harness covering staging resets, style refresh, cancellation, eight in-flight pause/resume cycles, last-consumer release, and shutdown. --- common/lc_assetloader.cpp | 2060 ++++++++++++++++++------------------- common/lc_assetloader.h | 104 +- common/lc_library.cpp | 18 +- common/lc_library.h | 12 +- common/lc_model.cpp | 2 +- docs/asset-loading.md | 12 + 6 files changed, 1073 insertions(+), 1135 deletions(-) diff --git a/common/lc_assetloader.cpp b/common/lc_assetloader.cpp index 4843b81d..daf3a1d6 100644 --- a/common/lc_assetloader.cpp +++ b/common/lc_assetloader.cpp @@ -12,8 +12,10 @@ constexpr QEvent::Type lcAssetLoaderCompletionEvent = static_cast(QEvent::User + 1); -lcPartBuildResult::~lcPartBuildResult() = default; +lcPieceBuildResult::~lcPieceBuildResult() = default; +// Normal part flow: enqueue -> build -> resolve colors -> convert -> settle textures -> publish -> notify. +// Workers own private build data. Publication and deferred notifications stay on the UI thread. lcAssetLoader::lcAssetLoader(lcPiecesLibrary* Library) : mLibrary(Library) { @@ -46,245 +48,11 @@ bool lcAssetLoader::LoadPieceInfo(PieceInfo* Info, lcPieceLoadFlags Flags) return !Flags.testFlag(lcPieceLoadFlag::Wait) || WaitForRequest(Info); } -void lcAssetLoader::QueuePiece(PieceInfo* Info, bool PriorityHint) +void lcAssetLoader::QueuePiece(PieceInfo* Info) { Q_ASSERT(QThread::currentThread() == thread()); - EnqueuePiece(Info, PriorityHint ? Priority::Visible : Priority::Background); -} - -void lcAssetLoader::CancelSynthMesh(lcPiece* Piece) -{ - // Remove queued work immediately; running synth phases notice cancellation cooperatively. - Q_ASSERT(QThread::currentThread() == thread()); - - mSynthPieces.erase(Piece); - mFailedSynthErrors.erase(Piece); - - for (SynthNotification& Pending : mSynthNotifications) - if (Pending.RequestedSynth->Piece == Piece) - Pending.RequestedSynth->Cancelled.store(true, std::memory_order_relaxed); - - std::shared_ptr Removed; - - { - QMutexLocker QueueLock(&mQueueMutex); - const SynthRequestMap::iterator It = mSynthRequests.find(Piece); - - if (It == mSynthRequests.end()) - return; - - Removed = It->second; - Removed->Cancelled.store(true, std::memory_order_relaxed); - Removed->Terminal = true; - mSynthRequests.erase(It); - - const auto EraseRequest = [&Removed](auto& Queue) - { - Queue.erase(std::remove(Queue.begin(), Queue.end(), Removed), Queue.end()); - }; - - EraseRequest(mSynthQueue); - EraseRequest(mPausedSynthQueue); - mResultReady.wakeAll(); - } - - if (!Removed->Running) - { - Removed->StagedMesh.reset(); - CancelUnusedTextureRequests(); - } -} - -void lcAssetLoader::QueueSynthMesh(lcPiece* Piece) -{ - Q_ASSERT(QThread::currentThread() == thread()); - - CancelSynthMesh(Piece); - - PieceInfo* Info = Piece->mPieceInfo; - const std::shared_ptr SynthInfo = Info ? Info->GetSynthInfoShared() : nullptr; - - if (!SynthInfo) - return; - - std::shared_ptr RequestedSynth = std::make_shared(); - RequestedSynth->Piece = Piece; - RequestedSynth->SynthInfo = SynthInfo; - RequestedSynth->ControlPoints = Piece->GetControlPoints(); - RequestedSynth->LoadPriority = Priority::Visible; - RequestedSynth->EnqueuedAt = QDateTime::currentMSecsSinceEpoch(); - RequestedSynth->Generation = mGeneration; - - QMutexLocker QueueLock(&mQueueMutex); - - if (mStopping) - return; - - mSynthPieces.insert(Piece); - mSynthRequests.emplace(Piece, RequestedSynth); - - if (mPaused) - mPausedSynthQueue.push_back(RequestedSynth); - else - { - mSynthQueue.push_back(RequestedSynth); - StartWorkersLocked(); - } - - QueueLock.unlock(); - emit mLibrary->AssetRequestsChanged(); -} - -bool lcAssetLoader::EnsureSynthMeshesReady(const std::vector& Pieces) -{ - Q_ASSERT(QThread::currentThread() == thread()); - - std::vector> Required; - - { - QMutexLocker QueueLock(&mQueueMutex); - - for (lcPiece* Piece : Pieces) - { - const SynthRequestMap::iterator It = mSynthRequests.find(Piece); - - if (It == mSynthRequests.end()) - continue; - - const std::shared_ptr& RequestedSynth = It->second; - RequestedSynth->LoadPriority = Priority::Blocking; - Required.push_back(RequestedSynth); - - if (RequestedSynth->StagedMesh) - for (const lcMeshLod& Lod : RequestedSynth->StagedMesh->mLods) - for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) - { - const TextureRequestMap::iterator TextureIt = mTextureRequests.find(Lod.Sections[SectionIdx].Texture); - - if (TextureIt != mTextureRequests.end()) - TextureIt->second->LoadPriority = Priority::Blocking; - } - } - - StartWorkersLocked(); - } - - QOpenGLContext* const PreviousContext = QOpenGLContext::currentContext(); - QSurface* const PreviousSurface = PreviousContext ? PreviousContext->surface() : nullptr; - - for (const std::shared_ptr& RequestedSynth : Required) - { - while (!RequestedSynth->Terminal) - { - ProcessCompletions(); - - if (RequestedSynth->Terminal) - break; - - QMutexLocker QueueLock(&mQueueMutex); - - if (mStopping || RequestedSynth->Terminal) - break; - - WaitForResultsLocked(); - } - } - - if (PreviousContext && PreviousSurface && (QOpenGLContext::currentContext() != PreviousContext || PreviousContext->surface() != PreviousSurface)) - PreviousContext->makeCurrent(PreviousSurface); - - for (lcPiece* Piece : Pieces) - if (Piece->mPieceInfo && Piece->mPieceInfo->GetSynthInfo() && !Piece->HasGeneratedMesh()) - return false; - - return true; -} - -QString lcAssetLoader::GetSynthMeshError(const lcPiece* Piece) const -{ - const std::unordered_map::const_iterator It = mFailedSynthErrors.find(Piece); - - return It != mFailedSynthErrors.end() ? It->second : QString(); -} - -void lcAssetLoader::ReloadSynthMeshes() -{ - Q_ASSERT(QThread::currentThread() == thread()); - - const std::vector Pieces(mSynthPieces.begin(), mSynthPieces.end()); - - for (lcPiece* Piece : Pieces) - Piece->RefreshGeneratedMesh(); -} - -void lcAssetLoader::PromoteRequestPriorityLocked(const std::shared_ptr& RequestedPart, Priority LoadPriority) -{ - RequestedPart->LoadPriority = std::max(RequestedPart->LoadPriority, LoadPriority); - - if (!RequestedPart->StagedMesh) - return; - - for (const lcMeshLod& Lod : RequestedPart->StagedMesh->mLods) - { - for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) - { - const auto It = mTextureRequests.find(Lod.Sections[SectionIdx].Texture); - - if (It != mTextureRequests.end()) - It->second->LoadPriority = std::max(It->second->LoadPriority, RequestedPart->LoadPriority); - } - } -} - -void lcAssetLoader::OnConsumerReleased(PieceInfo* Info) -{ - // Ordinary running builds finish privately; discard them if their last consumer is gone. - Q_ASSERT(QThread::currentThread() == thread()); - - if (mLibrary->HasPieceConsumers(Info)) - return; - - std::shared_ptr Removed; - { - QMutexLocker QueueLock(&mQueueMutex); - - const auto It = mRequests.find(Info); - - if (It == mRequests.end()) - return; - - const std::shared_ptr& RequestedPart = It->second; - - if (RequestedPart->Running) - return; - - RequestedPart->Obsolete = true; - - Removed = RequestedPart; - Removed->Terminal = true; - Removed->Succeeded = false; - - Info->mState = lcPieceInfoState::Cancelled; - - mRequests.erase(It); - - auto EraseRequest = [&Removed](auto& Queue) - { - Queue.erase(std::remove(Queue.begin(), Queue.end(), Removed), Queue.end()); - }; - - EraseRequest(mQueue); - EraseRequest(mPausedQueue); - - mResultReady.wakeAll(); - } - - Removed->StagedMesh.reset(); - - CancelUnusedTextureRequests(); - - mLibrary->ReleasePieceLoadHold(Info); + EnqueuePiece(Info, Priority::Background); } void lcAssetLoader::EnqueuePiece(PieceInfo* Info, Priority LoadPriority) @@ -321,28 +89,109 @@ void lcAssetLoader::EnqueuePiece(PieceInfo* Info, Priority LoadPriority) mLibrary->ClearPieceLoadError(Info); - std::shared_ptr RequestedPart = std::make_shared(); + std::shared_ptr RequestedPiece = std::make_shared(); - RequestedPart->Info = Info; - RequestedPart->Generation = mGeneration; - RequestedPart->Source = mLibrary->SnapshotPieceSource(Info); - RequestedPart->LoadPriority = LoadPriority; - RequestedPart->EnqueuedAt = QDateTime::currentMSecsSinceEpoch(); + RequestedPiece->Info = Info; + RequestedPiece->Generation = mGeneration; + RequestedPiece->Source = mLibrary->SnapshotPieceSource(Info); + RequestedPiece->LoadPriority = LoadPriority; + RequestedPiece->EnqueuedAt = QDateTime::currentMSecsSinceEpoch(); - mRequests.emplace(Info, RequestedPart); + mRequests.emplace(Info, RequestedPiece); - if (mPaused) - mPausedQueue.push_back(RequestedPart); - else - { - mQueue.push_back(RequestedPart); - - StartWorkersLocked(); - } + mQueue.push_back(RequestedPiece); + StartWorkersLocked(); emit mLibrary->AssetRequestsChanged(); } +void lcAssetLoader::PromoteRequestPriorityLocked(const std::shared_ptr& RequestedPiece, Priority LoadPriority) +{ + RequestedPiece->LoadPriority = std::max(RequestedPiece->LoadPriority, LoadPriority); + + if (!RequestedPiece->StagedMesh) + return; + + for (const lcMeshLod& Lod : RequestedPiece->StagedMesh->mLods) + { + for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) + { + const auto It = mTextureRequests.find(Lod.Sections[SectionIdx].Texture); + + if (It != mTextureRequests.end()) + It->second->LoadPriority = std::max(It->second->LoadPriority, RequestedPiece->LoadPriority); + } + } +} + +void lcAssetLoader::QueueSynthMesh(lcPiece* Piece) +{ + Q_ASSERT(QThread::currentThread() == thread()); + + CancelSynthMesh(Piece); + + PieceInfo* Info = Piece->mPieceInfo; + const std::shared_ptr SynthInfo = Info ? Info->GetSynthInfoShared() : nullptr; + + if (!SynthInfo) + return; + + std::shared_ptr RequestedSynth = std::make_shared(); + RequestedSynth->Piece = Piece; + RequestedSynth->SynthInfo = SynthInfo; + RequestedSynth->ControlPoints = Piece->GetControlPoints(); + RequestedSynth->LoadPriority = Priority::Visible; + RequestedSynth->EnqueuedAt = QDateTime::currentMSecsSinceEpoch(); + RequestedSynth->Generation = mGeneration; + + QMutexLocker QueueLock(&mQueueMutex); + + if (mStopping) + return; + + mSynthPieces.insert(Piece); + mSynthRequests.emplace(Piece, RequestedSynth); + + mSynthQueue.push_back(RequestedSynth); + StartWorkersLocked(); + + QueueLock.unlock(); + emit mLibrary->AssetRequestsChanged(); +} + +void lcAssetLoader::QueueTexture(lcTexture* Texture, Priority LoadPriority) +{ + QMutexLocker QueueLock(&mQueueMutex); + + if (mStopping || Texture->IsReady() || Texture->mState == lcTextureState::Failed) + return; + + const auto Existing = mTextureRequests.find(Texture); + + if (Existing != mTextureRequests.end()) + { + if (Existing->second->LoadPriority < LoadPriority) + Existing->second->LoadPriority = LoadPriority; + return; + } + + mLibrary->AddTextureReference(Texture); + Texture->mState = lcTextureState::Queued; + + std::shared_ptr RequestedTexture = std::make_shared(); + + RequestedTexture->Texture = Texture; + RequestedTexture->Generation = mGeneration; + RequestedTexture->Source = Texture->mSource; + RequestedTexture->LoadPriority = LoadPriority; + RequestedTexture->EnqueuedAt = QDateTime::currentMSecsSinceEpoch(); + + mTextureRequests.emplace(Texture, RequestedTexture); + + mTextureQueue.push_back(RequestedTexture); + StartWorkersLocked(); +} + void lcAssetLoader::StartWorkersLocked() { mFutures.erase(std::remove_if(mFutures.begin(), mFutures.end(), [](const QFuture& Future) @@ -357,15 +206,15 @@ void lcAssetLoader::StartWorkersLocked() for (int Worker = 0; Worker < Additional; Worker++) { mActiveWorkers++; - mFutures.emplace_back(QtConcurrent::run([this]() { LoadQueuedPieces(); })); + mFutures.emplace_back(QtConcurrent::run([this]() { RunWorker(); })); } } -void lcAssetLoader::LoadQueuedPieces() +void lcAssetLoader::RunWorker() { for (;;) { - std::shared_ptr RequestedPart; + std::shared_ptr RequestedPiece; std::shared_ptr RequestedTexture; std::shared_ptr RequestedSynth; @@ -414,33 +263,33 @@ void lcAssetLoader::LoadQueuedPieces() } else { - RequestedPart = *PartBest; + RequestedPiece = *PartBest; mQueue.erase(PartBest); - RequestedPart->Running = true; - mStarted.push_back(RequestedPart); + RequestedPiece->Running = true; + mStarted.push_back(RequestedPiece); } } - if (RequestedPart || RequestedTexture || RequestedSynth) + if (RequestedPiece || RequestedTexture || RequestedSynth) QCoreApplication::postEvent(this, new QEvent(lcAssetLoaderCompletionEvent)); Completion Completed; - if (RequestedPart) + if (RequestedPiece) { - Completed.RequestedPart = RequestedPart; + Completed.RequestedPiece = RequestedPiece; - if (RequestedPart->ConvertingMesh) + if (RequestedPiece->ConvertingMesh) { - Completed.Result.Mesh.reset(RequestedPart->MeshData->CreateMeshResolved(RequestedPart->ColorTranslucency, RequestedPart->DefaultColorIndex, nullptr)); - RequestedPart->MeshData.reset(); + Completed.Result.Mesh.reset(RequestedPiece->MeshData->CreateMeshResolved(RequestedPiece->ColorTranslucency, RequestedPiece->DefaultColorIndex, nullptr)); + RequestedPiece->MeshData.reset(); if (!Completed.Result.Mesh) - Completed.Result.Error = tr("Could not prepare piece %1 for rendering.").arg(RequestedPart->Source.FileName); + Completed.Result.Error = tr("Could not prepare piece %1 for rendering.").arg(RequestedPiece->Source.FileName); } else { - Completed.Result = mLibrary->BuildPieceData(RequestedPart->Source); + Completed.Result = mLibrary->BuildPieceData(RequestedPiece->Source); if (Completed.Result.CacheData) { @@ -451,7 +300,7 @@ void lcAssetLoader::LoadQueuedPieces() } } - Completed.Result.Generation = RequestedPart->Generation; + Completed.Result.Generation = RequestedPiece->Generation; } else if (RequestedTexture) { @@ -483,44 +332,378 @@ void lcAssetLoader::LoadQueuedPieces() } } -void lcAssetLoader::QueueTexture(lcTexture* Texture, Priority LoadPriority) +bool lcAssetLoader::ProcessCompletions() { - QMutexLocker QueueLock(&mQueueMutex); + // Bounded UI-thread publication also runs during scoped waits, without public callbacks. + Q_ASSERT(QThread::currentThread() == thread()); - if (mStopping || Texture->IsReady() || Texture->mState == lcTextureState::Failed) - return; + bool Progress = false; - const auto Existing = mTextureRequests.find(Texture); - - if (Existing != mTextureRequests.end()) { - if (Existing->second->LoadPriority < LoadPriority) - Existing->second->LoadPriority = LoadPriority; + QMutexLocker QueueLock(&mQueueMutex); + + while (!mStarted.empty()) + { + const std::shared_ptr RequestedPiece = mStarted.front(); + + mStarted.pop_front(); + + const auto It = mRequests.find(RequestedPiece->Info); + + if (!mStopping && It != mRequests.end() && It->second == RequestedPiece && RequestedPiece->Info->mState == lcPieceInfoState::Queued) + RequestedPiece->Info->mState = lcPieceInfoState::Loading; + } + + while (!mStartedTextures.empty()) + { + const std::shared_ptr RequestedTexture = mStartedTextures.front(); + + mStartedTextures.pop_front(); + + const auto It = mTextureRequests.find(RequestedTexture->Texture); + + if (!mStopping && It != mTextureRequests.end() && It->second == RequestedTexture && RequestedTexture->Texture->mState == lcTextureState::Queued) + RequestedTexture->Texture->mState = lcTextureState::Decoding; + } + + } + + constexpr int MaxCompletionsPerPump = 16; + + for (int Processed = 0; Processed < MaxCompletionsPerPump; Processed++) + { + Completion Completed; + std::shared_ptr Retry; + + { + QMutexLocker QueueLock(&mQueueMutex); + + const qint64 Now = QDateTime::currentMSecsSinceEpoch(); + + for (const TextureRequestMap::value_type& Entry : mTextureRequests) + { + const std::shared_ptr& Candidate = Entry.second; + + if (Candidate->RetryAt && Candidate->RetryAt <= Now && + (!Retry || Candidate->LoadPriority > Retry->LoadPriority || + (Candidate->LoadPriority == Retry->LoadPriority && Candidate->RetryAt < Retry->RetryAt))) + Retry = Candidate; + } + + const auto PriorityOf = [](const Completion& Item) + { + if (Item.RequestedPiece) + return Item.RequestedPiece->LoadPriority; + + return Item.RequestedTexture ? Item.RequestedTexture->LoadPriority : Item.RequestedSynth->LoadPriority; + }; + + const auto Best = std::max_element(mCompletions.begin(), mCompletions.end(), [&PriorityOf](const Completion& Left, const Completion& Right) + { + return PriorityOf(Left) < PriorityOf(Right); + }); + + if (Retry && (Best == mCompletions.end() || Retry->LoadPriority >= PriorityOf(*Best))) + Retry->RetryAt = 0; + else + { + Retry.reset(); + + if (Best == mCompletions.end()) + return Progress; + + Completed = std::move(*Best); + + mCompletions.erase(Best); + } + } + + Progress = true; + + if (Retry) + { + UploadTextureRequest(Retry); + + continue; + } + + if (Completed.RequestedTexture) + CompleteTexture(Completed); + else if (Completed.RequestedSynth) + CompleteSynth(Completed); + else + CompletePiece(Completed); + } + + mQueueMutex.lock(); + const qint64 Now = QDateTime::currentMSecsSinceEpoch(); + const bool RetryDue = std::any_of(mTextureRequests.begin(), mTextureRequests.end(), [Now](const auto& Entry) + { + return Entry.second->RetryAt && Entry.second->RetryAt <= Now; + }); + const bool NeedsPump = !mCompletions.empty() || RetryDue; + mQueueMutex.unlock(); + if (NeedsPump) + QCoreApplication::postEvent(this, new QEvent(lcAssetLoaderCompletionEvent)); + return Progress; +} + +void lcAssetLoader::CompletePiece(Completion& Completed) +{ + const std::shared_ptr& RequestedPiece = Completed.RequestedPiece; + PieceInfo* Info = RequestedPiece->Info; + bool Stale; + + mQueueMutex.lock(); + const auto It = mRequests.find(Info); + Stale = mStopping || RequestedPiece->Obsolete || Completed.Result.Generation != mGeneration || It == mRequests.end() || It->second != RequestedPiece; + if (Stale && It != mRequests.end() && It->second == RequestedPiece) + mRequests.erase(It); + mQueueMutex.unlock(); + + RequestedPiece->Running = false; + + const bool HasConsumer = mLibrary->HasPieceConsumers(Info); + + if (Stale || !HasConsumer) + { + if (!Stale) + { + mQueueMutex.lock(); + mRequests.erase(Info); + mQueueMutex.unlock(); + } + + RequestedPiece->Terminal = true; + RequestedPiece->Succeeded = false; + + if (HasConsumer && !Stale) + Info->mState = lcPieceInfoState::Cancelled; + + mResultReady.wakeAll(); + + mLibrary->ReleasePieceLoadHold(Info); + return; } - mLibrary->AddTextureReference(Texture); - Texture->mState = lcTextureState::Queued; + RequestedPiece->Error = Completed.Result.Error; + RequestedPiece->EmptyGeometry = Completed.Result.EmptyGeometry; - std::shared_ptr RequestedTexture = std::make_shared(); + if (RequestedPiece->Error.isEmpty()) + { + if (Completed.Result.CacheData && !Completed.Result.Mesh) + { + // Rebuild a corrupt cache privately on a worker, then use the normal commit path. + RequestedPiece->Source.SkipCache = true; + Info->mState = lcPieceInfoState::Queued; - RequestedTexture->Texture = Texture; - RequestedTexture->Generation = mGeneration; - RequestedTexture->Source = Texture->mSource; - RequestedTexture->LoadPriority = LoadPriority; - RequestedTexture->EnqueuedAt = QDateTime::currentMSecsSinceEpoch(); + QMutexLocker QueueLock(&mQueueMutex); - mTextureRequests.emplace(Texture, RequestedTexture); + mQueue.push_back(RequestedPiece); + StartWorkersLocked(); - if (mPaused) - mPausedTextureQueue.push_back(RequestedTexture); + return; + } + else if (Completed.Result.MeshData) + { + // Register color indices on the UI thread, then queue the expensive mesh conversion. + ResolveMeshColors(*Completed.Result.MeshData, RequestedPiece->ColorTranslucency, RequestedPiece->DefaultColorIndex); + RequestedPiece->MeshData = std::move(Completed.Result.MeshData); + RequestedPiece->ConvertingMesh = true; + Info->mState = lcPieceInfoState::Queued; + + QMutexLocker QueueLock(&mQueueMutex); + + mQueue.push_back(RequestedPiece); + StartWorkersLocked(); + + return; + } + else if (Completed.Result.Mesh) + { + RequestedPiece->ConvertingMesh = false; + RequestedPiece->ColorTranslucency.clear(); + RequestedPiece->StagedMesh = std::move(Completed.Result.Mesh); + size_t ColorIndex = 0; + + for (lcMeshLod& Lod : RequestedPiece->StagedMesh->mLods) + { + for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) + { + lcMeshSection& Section = Lod.Sections[SectionIdx]; + + if (Completed.Result.CacheData) + Section.ColorIndex = lcGetColorIndex(Completed.Result.ColorCodes[ColorIndex++]); + } + } + + RequestedPiece->SaveCache = !Completed.Result.CacheData; + } + } + + // A private mesh cannot be published until all its declared textures settle. + if (RequestedPiece->StagedMesh) + { + const bool Ready = ResolveAndQueueMeshTextures(*RequestedPiece->StagedMesh, RequestedPiece->Source.TextureSearchDirectories, + RequestedPiece->Source.EmbeddedData, RequestedPiece->LoadPriority, RequestedPiece->Error); + + if (RequestedPiece->Error.isEmpty() && !Ready) + { + Info->mState = lcPieceInfoState::AwaitingTextures; + + return; + } + } + + FinishPiece(RequestedPiece); +} + +void lcAssetLoader::CompleteSynth(Completion& Completed) +{ + const std::shared_ptr& RequestedSynth = Completed.RequestedSynth; + bool Stale; + + mQueueMutex.lock(); + const SynthRequestMap::iterator It = mSynthRequests.find(RequestedSynth->Piece); + Stale = mStopping || RequestedSynth->Cancelled.load(std::memory_order_relaxed) || RequestedSynth->Generation != mGeneration || + It == mSynthRequests.end() || It->second != RequestedSynth; + if (Stale && It != mSynthRequests.end() && It->second == RequestedSynth) + mSynthRequests.erase(It); + RequestedSynth->Running = false; + mResultReady.wakeAll(); + mQueueMutex.unlock(); + + if (Stale) + { + RequestedSynth->Terminal = true; + return; + } + + if (Completed.SynthMeshData) + { + // Resolve global colors here, then send expensive conversion back to a worker. + ResolveMeshColors(*Completed.SynthMeshData, RequestedSynth->ColorTranslucency, RequestedSynth->DefaultColorIndex); + RequestedSynth->MeshData = std::move(Completed.SynthMeshData); + RequestedSynth->ConvertingMesh = true; + + QMutexLocker QueueLock(&mQueueMutex); + + mSynthQueue.push_back(RequestedSynth); + StartWorkersLocked(); + + return; + } + + if (!Completed.SynthMesh) + { + RequestedSynth->Error = tr("Could not generate geometry for piece '%1'.").arg(QString::fromLatin1(RequestedSynth->Piece->mPieceInfo->mFileName)); + FinishSynth(RequestedSynth); + return; + } + + RequestedSynth->ConvertingMesh = false; + RequestedSynth->ColorTranslucency.clear(); + RequestedSynth->StagedMesh = std::move(Completed.SynthMesh); + + const bool Ready = ResolveAndQueueMeshTextures(*RequestedSynth->StagedMesh, std::vector(), + std::map>(), RequestedSynth->LoadPriority, RequestedSynth->Error); + + if (!RequestedSynth->Error.isEmpty() || Ready) + FinishSynth(RequestedSynth); +} + +void lcAssetLoader::CompleteTexture(Completion& Completed) +{ + const std::shared_ptr& RequestedTexture = Completed.RequestedTexture; + lcTexture* Texture = RequestedTexture->Texture; + bool Stale; + + mQueueMutex.lock(); + const auto It = mTextureRequests.find(Texture); + Stale = mStopping || RequestedTexture->Generation != mGeneration || + It == mTextureRequests.end() || It->second != RequestedTexture; + if (Stale && It != mTextureRequests.end() && It->second == RequestedTexture) + mTextureRequests.erase(It); + RequestedTexture->Running = false; + mResultReady.wakeAll(); + mQueueMutex.unlock(); + + if (Stale) + mLibrary->ReleaseTexture(Texture); + else if (!Completed.TextureResult.DecodedImage.isNull()) + { + Texture->SetImage(std::move(Completed.TextureResult.DecodedImage), LC_TEXTURE_MIPMAPS); + if (Texture->NeedsUpload()) + UploadTextureRequest(RequestedTexture); + else + FinishTextureRequest(RequestedTexture); + } else { - mTextureQueue.push_back(RequestedTexture); - StartWorkersLocked(); + Texture->SetFailed(Completed.TextureResult.Error, std::move(Completed.TextureResult.ErrorDetails)); + + FinishTextureRequest(RequestedTexture); } } +void lcAssetLoader::ResolveMeshColors(lcLibraryMeshData& MeshData, std::vector& ColorTranslucency, int& DefaultColorIndex) +{ + // Global color registration belongs on the UI thread; conversion uses this copied palette. + MeshData.ResolveColors(); + ColorTranslucency.clear(); + ColorTranslucency.reserve(gColorList.size()); + + for (const lcColor& Color : gColorList) + ColorTranslucency.push_back(Color.Translucent); + + DefaultColorIndex = gDefaultColor; +} + +bool lcAssetLoader::ResolveAndQueueMeshTextures(lcMesh& Mesh, const std::vector& SearchDirectories, + const std::map>& EmbeddedData, Priority LoadPriority, QString& Error) +{ + for (lcMeshLod& Lod : Mesh.mLods) + for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) + { + lcMeshSection& Section = Lod.Sections[SectionIdx]; + + if (Section.TextureName.isEmpty()) + continue; + + Section.Texture = mLibrary->FindTextureDeferred(Section.TextureName.toLatin1().constData(), SearchDirectories, EmbeddedData); + + if (Section.Texture && !Section.Texture->IsReady() && Section.Texture->mState != lcTextureState::Failed) + QueueTexture(Section.Texture, LoadPriority); + } + + return MeshTexturesSettled(Mesh, Error); +} + +bool lcAssetLoader::MeshTexturesSettled(const lcMesh& Mesh, QString& Error) +{ + bool Ready = true; + + for (const lcMeshLod& Lod : Mesh.mLods) + for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) + { + const lcMeshSection& Section = Lod.Sections[SectionIdx]; + lcTexture* Texture = Section.Texture; + + if (!Texture) + { + if (!Section.TextureName.isEmpty()) + Error = tr("Missing texture %1.").arg(Section.TextureName); + } + else if (Texture->mState == lcTextureState::Failed) + Error = TextureFailureMessage(Texture); + else if (!Texture->IsReady()) + Ready = false; + } + + return Ready; +} + QString lcAssetLoader::TextureFailureMessage(const lcTexture* Texture) { if (!Texture->mLoadFailureDetails.isEmpty()) @@ -547,223 +730,6 @@ QString lcAssetLoader::TextureFailureMessage(const lcTexture* Texture) return tr("Texture %1 failed to load.").arg(QString::fromLatin1(Texture->mName)); } -void lcAssetLoader::CancelUnusedTextureRequests() -{ - std::vector> Cancelled; - - { - QMutexLocker QueueLock(&mQueueMutex); - - for (auto It = mTextureRequests.begin(); It != mTextureRequests.end();) - { - const std::shared_ptr& RequestedTexture = It->second; - - // A queued request owns one reference; all others belong to consumers. - if (RequestedTexture->Running || RequestedTexture->Texture->GetRefCount() > 1) - { - ++It; - continue; - } - - auto EraseRequest = [&RequestedTexture](auto& Queue) - { - Queue.erase(std::remove(Queue.begin(), Queue.end(), RequestedTexture), Queue.end()); - }; - - EraseRequest(mTextureQueue); - EraseRequest(mPausedTextureQueue); - - Cancelled.push_back(RequestedTexture); - - It = mTextureRequests.erase(It); - } - - if (!Cancelled.empty()) - mResultReady.wakeAll(); - } - - for (const std::shared_ptr& RequestedTexture : Cancelled) - { - lcTexture* Texture = RequestedTexture->Texture; - - Texture->DiscardDecodedImage(); - Texture->mState = lcTextureState::Unrequested; - Texture->mLoadFailure = lcTextureLoadError::None; - Texture->mLoadFailureDetails.clear(); - - mLibrary->ReleaseTexture(Texture); - } -} - -void lcAssetLoader::FinishPart(const std::shared_ptr& RequestedPart) -{ - PieceInfo* Info = RequestedPart->Info; - const bool Loaded = RequestedPart->Error.isEmpty() && (RequestedPart->StagedMesh || RequestedPart->EmptyGeometry); - QString LoadError; - - if (Loaded) - { - if (RequestedPart->SaveCache && RequestedPart->StagedMesh) - mLibrary->SaveBuiltPieceCache(RequestedPart->Source, *RequestedPart->StagedMesh); - - if (RequestedPart->StagedMesh) - Info->SetMesh(RequestedPart->StagedMesh.release()); - - Info->mState = lcPieceInfoState::Loaded; - mLibrary->ClearPieceLoadError(Info); - } - else - { - LoadError = RequestedPart->Error.isEmpty() ? tr("Could not load part %1.").arg(RequestedPart->Source.FileName) : RequestedPart->Error; - - Info->SetFailed(LoadError); - } - - mQueueMutex.lock(); - const auto It = mRequests.find(Info); - if (It != mRequests.end() && It->second == RequestedPart) - mRequests.erase(It); - RequestedPart->Terminal = true; - RequestedPart->Succeeded = Loaded; - mResultReady.wakeAll(); - mQueueMutex.unlock(); - - mLibrary->ScheduleBufferRepack(); - mNotifications.push_back({ RequestedPart, Loaded, std::move(LoadError) }); - - QCoreApplication::postEvent(this, new QEvent(lcAssetLoaderCompletionEvent)); -} - -void lcAssetLoader::CheckWaitingParts() -{ - std::vector> Waiting; - - { - QMutexLocker QueueLock(&mQueueMutex); - - for (const PartRequestMap::value_type& Entry : mRequests) - if (Entry.second->StagedMesh) - Waiting.push_back(Entry.second); - } - - for (const std::shared_ptr& RequestedPart : Waiting) - { - bool Ready = true; - - for (const lcMeshLod& Lod : RequestedPart->StagedMesh->mLods) - { - for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) - { - lcTexture* Texture = Lod.Sections[SectionIdx].Texture; - - if (!Texture) - continue; - - if (Texture->mState == lcTextureState::Failed) - RequestedPart->Error = TextureFailureMessage(Texture); - else if (!Texture->IsReady()) - Ready = false; - } - } - - if (!RequestedPart->Error.isEmpty() || Ready) - FinishPart(RequestedPart); - } -} - -void lcAssetLoader::FinishSynth(const std::shared_ptr& RequestedSynth) -{ - lcPiece* Piece = RequestedSynth->Piece; - PieceInfo* Info = Piece->mPieceInfo; - const bool Loaded = RequestedSynth->Error.isEmpty() && RequestedSynth->StagedMesh; - - Piece->SetGeneratedMesh(Loaded ? RequestedSynth->StagedMesh.release() : nullptr); - - if (Loaded) - mFailedSynthErrors.erase(Piece); - else - mFailedSynthErrors.insert_or_assign(Piece, RequestedSynth->Error); - - if (!Loaded) - { - RequestedSynth->StagedMesh.reset(); - CancelUnusedTextureRequests(); - } - - mQueueMutex.lock(); - const SynthRequestMap::iterator It = mSynthRequests.find(Piece); - - if (It != mSynthRequests.end() && It->second == RequestedSynth) - mSynthRequests.erase(It); - - RequestedSynth->Terminal = true; - mResultReady.wakeAll(); - mQueueMutex.unlock(); - - mLibrary->AddPieceReference(Info); - mSynthNotifications.push_back({ RequestedSynth, Info }); - - QCoreApplication::postEvent(this, new QEvent(lcAssetLoaderCompletionEvent)); -} - -void lcAssetLoader::CheckWaitingSynths() -{ - std::vector> Waiting; - - { - QMutexLocker QueueLock(&mQueueMutex); - - for (const SynthRequestMap::value_type& Entry : mSynthRequests) - if (Entry.second->StagedMesh) - Waiting.push_back(Entry.second); - } - - for (const std::shared_ptr& RequestedSynth : Waiting) - { - mQueueMutex.lock(); - const SynthRequestMap::iterator It = mSynthRequests.find(RequestedSynth->Piece); - const bool Current = It != mSynthRequests.end() && It->second == RequestedSynth; - mQueueMutex.unlock(); - - if (!Current) - continue; - - bool Ready = true; - - for (const lcMeshLod& Lod : RequestedSynth->StagedMesh->mLods) - for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) - { - lcTexture* Texture = Lod.Sections[SectionIdx].Texture; - - if (!Texture) - continue; - - if (Texture->mState == lcTextureState::Failed) - RequestedSynth->Error = TextureFailureMessage(Texture); - else if (!Texture->IsReady()) - Ready = false; - } - - if (!RequestedSynth->Error.isEmpty() || Ready) - FinishSynth(RequestedSynth); - } -} - -void lcAssetLoader::FinishTextureRequest(const std::shared_ptr& RequestedTexture) -{ - mQueueMutex.lock(); - const auto It = mTextureRequests.find(RequestedTexture->Texture); - if (It != mTextureRequests.end() && It->second == RequestedTexture) - mTextureRequests.erase(It); - mResultReady.wakeAll(); - mQueueMutex.unlock(); - - mLibrary->ReleaseTexture(RequestedTexture->Texture); - - CheckWaitingParts(); - CheckWaitingSynths(); -} - void lcAssetLoader::UploadTextureRequest(const std::shared_ptr& RequestedTexture) { lcTexture* Texture = RequestedTexture->Texture; @@ -831,419 +797,143 @@ void lcAssetLoader::UploadTextureRequest(const std::shared_ptr& FinishTextureRequest(RequestedTexture); } -void lcAssetLoader::WaitForResultsLocked() +void lcAssetLoader::FinishTextureRequest(const std::shared_ptr& RequestedTexture) { - if (!mCompletions.empty()) - return; + mQueueMutex.lock(); + const auto It = mTextureRequests.find(RequestedTexture->Texture); + if (It != mTextureRequests.end() && It->second == RequestedTexture) + mTextureRequests.erase(It); + mResultReady.wakeAll(); + mQueueMutex.unlock(); - qint64 EarliestRetry = -1; + mLibrary->ReleaseTexture(RequestedTexture->Texture); - for (const TextureRequestMap::value_type& Entry : mTextureRequests) - { - const qint64 RetryAt = Entry.second->RetryAt; - - if (RetryAt && (EarliestRetry == -1 || RetryAt < EarliestRetry)) - EarliestRetry = RetryAt; - } - - if (EarliestRetry == -1) - mResultReady.wait(&mQueueMutex); - else - { - const qint64 Delay = EarliestRetry - QDateTime::currentMSecsSinceEpoch(); - - if (Delay > 0) - mResultReady.wait(&mQueueMutex, static_cast(Delay)); - } + FinishPiecesWithSettledTextures(); + FinishSynthsWithSettledTextures(); } -bool lcAssetLoader::ProcessCompletions() +void lcAssetLoader::FinishPiecesWithSettledTextures() { - // Bounded UI-thread publication also runs during scoped waits, without public callbacks. - Q_ASSERT(QThread::currentThread() == thread()); - - bool Progress = false; + std::vector> Waiting; { QMutexLocker QueueLock(&mQueueMutex); - while (!mStarted.empty()) - { - const std::shared_ptr RequestedPart = mStarted.front(); - - mStarted.pop_front(); - - const auto It = mRequests.find(RequestedPart->Info); - - if (!mStopping && It != mRequests.end() && It->second == RequestedPart && RequestedPart->Info->mState == lcPieceInfoState::Queued) - RequestedPart->Info->mState = lcPieceInfoState::Loading; - } - - while (!mStartedTextures.empty()) - { - const std::shared_ptr RequestedTexture = mStartedTextures.front(); - - mStartedTextures.pop_front(); - - const auto It = mTextureRequests.find(RequestedTexture->Texture); - - if (!mStopping && It != mTextureRequests.end() && It->second == RequestedTexture && RequestedTexture->Texture->mState == lcTextureState::Queued) - RequestedTexture->Texture->mState = lcTextureState::Decoding; - } - + for (const PieceRequestMap::value_type& Entry : mRequests) + if (Entry.second->StagedMesh) + Waiting.push_back(Entry.second); } - constexpr int MaxCompletionsPerPump = 16; - - for (int Processed = 0; Processed < MaxCompletionsPerPump; Processed++) + for (const std::shared_ptr& RequestedPiece : Waiting) { - Completion Completed; - std::shared_ptr Retry; + const bool Ready = MeshTexturesSettled(*RequestedPiece->StagedMesh, RequestedPiece->Error); - { - QMutexLocker QueueLock(&mQueueMutex); + if (!RequestedPiece->Error.isEmpty() || Ready) + FinishPiece(RequestedPiece); + } +} - const qint64 Now = QDateTime::currentMSecsSinceEpoch(); +void lcAssetLoader::FinishSynthsWithSettledTextures() +{ + std::vector> Waiting; - for (const TextureRequestMap::value_type& Entry : mTextureRequests) - { - const std::shared_ptr& Candidate = Entry.second; + { + QMutexLocker QueueLock(&mQueueMutex); - if (Candidate->RetryAt && Candidate->RetryAt <= Now && - (!Retry || Candidate->LoadPriority > Retry->LoadPriority || - (Candidate->LoadPriority == Retry->LoadPriority && Candidate->RetryAt < Retry->RetryAt))) - Retry = Candidate; - } - - const auto PriorityOf = [](const Completion& Item) - { - if (Item.RequestedPart) - return Item.RequestedPart->LoadPriority; - - return Item.RequestedTexture ? Item.RequestedTexture->LoadPriority : Item.RequestedSynth->LoadPriority; - }; - - const auto Best = std::max_element(mCompletions.begin(), mCompletions.end(), [&PriorityOf](const Completion& Left, const Completion& Right) - { - return PriorityOf(Left) < PriorityOf(Right); - }); - - if (Retry && (Best == mCompletions.end() || Retry->LoadPriority >= PriorityOf(*Best))) - Retry->RetryAt = 0; - else - { - Retry.reset(); - - if (Best == mCompletions.end()) - return Progress; - - Completed = std::move(*Best); - - mCompletions.erase(Best); - } - } - - Progress = true; - - if (Retry) - { - UploadTextureRequest(Retry); - - continue; - } - - if (Completed.RequestedTexture) - { - const std::shared_ptr& RequestedTexture = Completed.RequestedTexture; - lcTexture* Texture = RequestedTexture->Texture; - bool Stale; - - mQueueMutex.lock(); - const auto It = mTextureRequests.find(Texture); - Stale = mStopping || RequestedTexture->Generation != mGeneration || - It == mTextureRequests.end() || It->second != RequestedTexture; - if (Stale && It != mTextureRequests.end() && It->second == RequestedTexture) - mTextureRequests.erase(It); - RequestedTexture->Running = false; - mResultReady.wakeAll(); - mQueueMutex.unlock(); - - if (Stale) - mLibrary->ReleaseTexture(Texture); - else if (!Completed.TextureResult.DecodedImage.isNull()) - { - Texture->SetImage(std::move(Completed.TextureResult.DecodedImage), LC_TEXTURE_MIPMAPS); - if (Texture->NeedsUpload()) - UploadTextureRequest(RequestedTexture); - else - FinishTextureRequest(RequestedTexture); - } - else - { - Texture->SetFailed(Completed.TextureResult.Error, std::move(Completed.TextureResult.ErrorDetails)); - - FinishTextureRequest(RequestedTexture); - } - - continue; - } - - if (Completed.RequestedSynth) - { - const std::shared_ptr& RequestedSynth = Completed.RequestedSynth; - bool Stale; - - mQueueMutex.lock(); - const SynthRequestMap::iterator It = mSynthRequests.find(RequestedSynth->Piece); - Stale = mStopping || RequestedSynth->Cancelled.load(std::memory_order_relaxed) || RequestedSynth->Generation != mGeneration || - It == mSynthRequests.end() || It->second != RequestedSynth; - if (Stale && It != mSynthRequests.end() && It->second == RequestedSynth) - mSynthRequests.erase(It); - RequestedSynth->Running = false; - mResultReady.wakeAll(); - mQueueMutex.unlock(); - - if (Stale) - { - RequestedSynth->Terminal = true; - continue; - } - - if (Completed.SynthMeshData) - { - Completed.SynthMeshData->ResolveColors(); - RequestedSynth->ColorTranslucency.clear(); - RequestedSynth->ColorTranslucency.reserve(gColorList.size()); - - for (const lcColor& Color : gColorList) - RequestedSynth->ColorTranslucency.push_back(Color.Translucent); - - RequestedSynth->DefaultColorIndex = gDefaultColor; - RequestedSynth->MeshData = std::move(Completed.SynthMeshData); - RequestedSynth->ConvertingMesh = true; - - QMutexLocker QueueLock(&mQueueMutex); - - if (mPaused) - mPausedSynthQueue.push_back(RequestedSynth); - else - { - mSynthQueue.push_back(RequestedSynth); - StartWorkersLocked(); - } - - continue; - } - - if (!Completed.SynthMesh) - { - RequestedSynth->Error = tr("Could not generate geometry for piece '%1'.").arg(QString::fromLatin1(RequestedSynth->Piece->mPieceInfo->mFileName)); - FinishSynth(RequestedSynth); - continue; - } - - RequestedSynth->ConvertingMesh = false; - RequestedSynth->ColorTranslucency.clear(); - RequestedSynth->StagedMesh = std::move(Completed.SynthMesh); - - bool Ready = true; - - for (lcMeshLod& Lod : RequestedSynth->StagedMesh->mLods) - for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) - { - lcMeshSection& Section = Lod.Sections[SectionIdx]; - - if (Section.TextureName.isEmpty()) - continue; - - Section.Texture = mLibrary->FindTextureDeferred(Section.TextureName.toLatin1().constData(), std::vector(), std::map>()); - - if (!Section.Texture) - RequestedSynth->Error = tr("Missing texture %1.").arg(Section.TextureName); - else if (Section.Texture->mState == lcTextureState::Failed) - RequestedSynth->Error = TextureFailureMessage(Section.Texture); - else if (!Section.Texture->IsReady()) - { - Ready = false; - QueueTexture(Section.Texture, RequestedSynth->LoadPriority); - } - } - - if (!RequestedSynth->Error.isEmpty() || Ready) - FinishSynth(RequestedSynth); - - continue; - } - - const std::shared_ptr& RequestedPart = Completed.RequestedPart; - PieceInfo* Info = RequestedPart->Info; - bool Stale; + for (const SynthRequestMap::value_type& Entry : mSynthRequests) + if (Entry.second->StagedMesh) + Waiting.push_back(Entry.second); + } + for (const std::shared_ptr& RequestedSynth : Waiting) + { mQueueMutex.lock(); - const auto It = mRequests.find(Info); - Stale = mStopping || RequestedPart->Obsolete || Completed.Result.Generation != mGeneration || It == mRequests.end() || It->second != RequestedPart; - if (Stale && It != mRequests.end() && It->second == RequestedPart) - mRequests.erase(It); + const SynthRequestMap::iterator It = mSynthRequests.find(RequestedSynth->Piece); + const bool Current = It != mSynthRequests.end() && It->second == RequestedSynth; mQueueMutex.unlock(); - RequestedPart->Running = false; - - const bool HasConsumer = mLibrary->HasPieceConsumers(Info); - - if (Stale || !HasConsumer) - { - if (!Stale) - { - mQueueMutex.lock(); - mRequests.erase(Info); - mQueueMutex.unlock(); - } - - RequestedPart->Terminal = true; - RequestedPart->Succeeded = false; - - if (HasConsumer && !Stale) - Info->mState = lcPieceInfoState::Cancelled; - - mResultReady.wakeAll(); - - mLibrary->ReleasePieceLoadHold(Info); - + if (!Current) continue; - } - RequestedPart->Error = Completed.Result.Error; - RequestedPart->EmptyGeometry = Completed.Result.EmptyGeometry; + const bool Ready = MeshTexturesSettled(*RequestedSynth->StagedMesh, RequestedSynth->Error); - if (RequestedPart->Error.isEmpty()) - { - const auto TextureLookup = [this, &RequestedPart](const char* Name) - { - return mLibrary->FindTextureDeferred(Name, RequestedPart->Source.TextureSearchDirectories, RequestedPart->Source.EmbeddedData); - }; + if (!RequestedSynth->Error.isEmpty() || Ready) + FinishSynth(RequestedSynth); + } +} - if (Completed.Result.CacheData && !Completed.Result.Mesh) - { - // Rebuild a corrupt cache privately on a worker, then use the normal commit path. - RequestedPart->Source.SkipCache = true; - Info->mState = lcPieceInfoState::Queued; +void lcAssetLoader::FinishPiece(const std::shared_ptr& RequestedPiece) +{ + PieceInfo* Info = RequestedPiece->Info; + const bool Loaded = RequestedPiece->Error.isEmpty() && (RequestedPiece->StagedMesh || RequestedPiece->EmptyGeometry); + QString LoadError; - QMutexLocker QueueLock(&mQueueMutex); + if (Loaded) + { + if (RequestedPiece->SaveCache && RequestedPiece->StagedMesh) + mLibrary->SaveBuiltPieceCache(RequestedPiece->Source, *RequestedPiece->StagedMesh); - if (mPaused) - mPausedQueue.push_back(RequestedPart); - else - { - mQueue.push_back(RequestedPart); - StartWorkersLocked(); - } + if (RequestedPiece->StagedMesh) + Info->SetMesh(RequestedPiece->StagedMesh.release()); - continue; - } - else if (Completed.Result.MeshData) - { - // Register color indices on the UI thread, then queue the expensive mesh conversion. - Completed.Result.MeshData->ResolveColors(); - RequestedPart->ColorTranslucency.clear(); - RequestedPart->ColorTranslucency.reserve(gColorList.size()); + Info->mState = lcPieceInfoState::Loaded; + mLibrary->ClearPieceLoadError(Info); + } + else + { + LoadError = RequestedPiece->Error.isEmpty() ? tr("Could not load part %1.").arg(RequestedPiece->Source.FileName) : RequestedPiece->Error; - for (const lcColor& Color : gColorList) - RequestedPart->ColorTranslucency.push_back(Color.Translucent); - - RequestedPart->DefaultColorIndex = gDefaultColor; - RequestedPart->MeshData = std::move(Completed.Result.MeshData); - RequestedPart->ConvertingMesh = true; - Info->mState = lcPieceInfoState::Queued; - - QMutexLocker QueueLock(&mQueueMutex); - - if (mPaused) - mPausedQueue.push_back(RequestedPart); - else - { - mQueue.push_back(RequestedPart); - StartWorkersLocked(); - } - - continue; - } - else if (Completed.Result.Mesh) - { - RequestedPart->ConvertingMesh = false; - RequestedPart->ColorTranslucency.clear(); - RequestedPart->StagedMesh = std::move(Completed.Result.Mesh); - size_t ColorIndex = 0; - - for (lcMeshLod& Lod : RequestedPart->StagedMesh->mLods) - { - for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) - { - lcMeshSection& Section = Lod.Sections[SectionIdx]; - - if (Completed.Result.CacheData) - Section.ColorIndex = lcGetColorIndex(Completed.Result.ColorCodes[ColorIndex++]); - - if (!Section.TextureName.isEmpty()) - Section.Texture = TextureLookup(Section.TextureName.toLatin1().constData()); - } - } - - RequestedPart->SaveCache = !Completed.Result.CacheData; - } - } - - if (RequestedPart->StagedMesh) - { - bool Ready = true; - - for (const lcMeshLod& Lod : RequestedPart->StagedMesh->mLods) - { - for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) - { - const lcMeshSection& Section = Lod.Sections[SectionIdx]; - - if (Section.TextureName.isEmpty()) - continue; - - if (!Section.Texture) - { - RequestedPart->Error = tr("Missing texture %1.").arg(Section.TextureName); - - continue; - } - - if (Section.Texture->mState == lcTextureState::Failed) - RequestedPart->Error = TextureFailureMessage(Section.Texture); - else if (!Section.Texture->IsReady()) - { - Ready = false; - QueueTexture(Section.Texture, RequestedPart->LoadPriority); - } - } - } - - if (RequestedPart->Error.isEmpty() && !Ready) - { - Info->mState = lcPieceInfoState::AwaitingTextures; - - continue; - } - } - - FinishPart(RequestedPart); + Info->SetFailed(LoadError); } mQueueMutex.lock(); - const qint64 Now = QDateTime::currentMSecsSinceEpoch(); - const bool RetryDue = std::any_of(mTextureRequests.begin(), mTextureRequests.end(), [Now](const auto& Entry) - { - return Entry.second->RetryAt && Entry.second->RetryAt <= Now; - }); - const bool NeedsPump = !mCompletions.empty() || RetryDue; + const auto It = mRequests.find(Info); + if (It != mRequests.end() && It->second == RequestedPiece) + mRequests.erase(It); + RequestedPiece->Terminal = true; + RequestedPiece->Succeeded = Loaded; + mResultReady.wakeAll(); mQueueMutex.unlock(); - if (NeedsPump) - QCoreApplication::postEvent(this, new QEvent(lcAssetLoaderCompletionEvent)); - return Progress; + + mLibrary->ScheduleBufferRepack(); + mNotifications.push_back({ RequestedPiece, Loaded, std::move(LoadError) }); + + QCoreApplication::postEvent(this, new QEvent(lcAssetLoaderCompletionEvent)); +} + +void lcAssetLoader::FinishSynth(const std::shared_ptr& RequestedSynth) +{ + lcPiece* Piece = RequestedSynth->Piece; + PieceInfo* Info = Piece->mPieceInfo; + const bool Loaded = RequestedSynth->Error.isEmpty() && RequestedSynth->StagedMesh; + + Piece->SetGeneratedMesh(Loaded ? RequestedSynth->StagedMesh.release() : nullptr); + + if (Loaded) + mFailedSynthErrors.erase(Piece); + else + mFailedSynthErrors.insert_or_assign(Piece, RequestedSynth->Error); + + if (!Loaded) + { + RequestedSynth->StagedMesh.reset(); + CancelUnusedTextureRequests(); + } + + mQueueMutex.lock(); + const SynthRequestMap::iterator It = mSynthRequests.find(Piece); + + if (It != mSynthRequests.end() && It->second == RequestedSynth) + mSynthRequests.erase(It); + + RequestedSynth->Terminal = true; + mResultReady.wakeAll(); + mQueueMutex.unlock(); + + mLibrary->AddPieceReference(Info); + mSynthNotifications.push_back({ RequestedSynth, Info }); + + QCoreApplication::postEvent(this, new QEvent(lcAssetLoaderCompletionEvent)); } void lcAssetLoader::DispatchNotifications() @@ -1259,9 +949,9 @@ void lcAssetLoader::DispatchNotifications() mNotifications.pop_front(); - PieceInfo* Info = Notification.RequestedPart->Info; + PieceInfo* Info = Notification.RequestedPiece->Info; - if (Notification.RequestedPart->Obsolete || !mLibrary->HasPieceConsumers(Info)) + if (Notification.RequestedPiece->Obsolete || !mLibrary->HasPieceConsumers(Info)) { mLibrary->ReleasePieceLoadHold(Info); @@ -1313,11 +1003,49 @@ void lcAssetLoader::DispatchNotifications() mLibrary->NotifyConsumersChanged(); } +bool lcAssetLoader::event(QEvent* Event) +{ + if (Event->type() == lcAssetLoaderCompletionEvent) + { + ProcessCompletions(); + DispatchNotifications(); + + return true; + } + + return QObject::event(Event); +} +void lcAssetLoader::WaitForResultsLocked() +{ + if (!mCompletions.empty()) + return; + + qint64 EarliestRetry = -1; + + for (const TextureRequestMap::value_type& Entry : mTextureRequests) + { + const qint64 RetryAt = Entry.second->RetryAt; + + if (RetryAt && (EarliestRetry == -1 || RetryAt < EarliestRetry)) + EarliestRetry = RetryAt; + } + + if (EarliestRetry == -1) + mResultReady.wait(&mQueueMutex); + else + { + const qint64 Delay = EarliestRetry - QDateTime::currentMSecsSinceEpoch(); + + if (Delay > 0) + mResultReady.wait(&mQueueMutex, static_cast(Delay)); + } +} + bool lcAssetLoader::WaitForRequest(PieceInfo* Info) { Q_ASSERT(QThread::currentThread() == thread()); - std::shared_ptr RequestedPart; + std::shared_ptr RequestedPiece; { QMutexLocker QueueLock(&mQueueMutex); @@ -1326,14 +1054,14 @@ bool lcAssetLoader::WaitForRequest(PieceInfo* Info) if (It != mRequests.end()) { - RequestedPart = It->second; + RequestedPiece = It->second; - PromoteRequestPriorityLocked(RequestedPart, Priority::Blocking); + PromoteRequestPriorityLocked(RequestedPiece, Priority::Blocking); } StartWorkersLocked(); } - if (!RequestedPart) + if (!RequestedPiece) return Info->mState == lcPieceInfoState::Loaded; QOpenGLContext* const PreviousContext = QOpenGLContext::currentContext(); @@ -1343,12 +1071,12 @@ bool lcAssetLoader::WaitForRequest(PieceInfo* Info) { ProcessCompletions(); - if (RequestedPart->Terminal) + if (RequestedPiece->Terminal) break; QMutexLocker QueueLock(&mQueueMutex); - if (mStopping || RequestedPart->Terminal) + if (mStopping || RequestedPiece->Terminal) break; WaitForResultsLocked(); @@ -1357,7 +1085,7 @@ bool lcAssetLoader::WaitForRequest(PieceInfo* Info) if (PreviousContext && PreviousSurface && (QOpenGLContext::currentContext() != PreviousContext || PreviousContext->surface() != PreviousSurface)) PreviousContext->makeCurrent(PreviousSurface); - return RequestedPart->Succeeded; + return RequestedPiece->Succeeded; } bool lcAssetLoader::EnsurePieceReady(PieceInfo* Info) @@ -1415,6 +1143,71 @@ bool lcAssetLoader::EnsurePiecesReady(const std::vector& Parts) return Ready; } +bool lcAssetLoader::EnsureSynthMeshesReady(const std::vector& Pieces) +{ + Q_ASSERT(QThread::currentThread() == thread()); + + std::vector> Required; + + { + QMutexLocker QueueLock(&mQueueMutex); + + for (lcPiece* Piece : Pieces) + { + const SynthRequestMap::iterator It = mSynthRequests.find(Piece); + + if (It == mSynthRequests.end()) + continue; + + const std::shared_ptr& RequestedSynth = It->second; + RequestedSynth->LoadPriority = Priority::Blocking; + Required.push_back(RequestedSynth); + + if (RequestedSynth->StagedMesh) + for (const lcMeshLod& Lod : RequestedSynth->StagedMesh->mLods) + for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++) + { + const TextureRequestMap::iterator TextureIt = mTextureRequests.find(Lod.Sections[SectionIdx].Texture); + + if (TextureIt != mTextureRequests.end()) + TextureIt->second->LoadPriority = Priority::Blocking; + } + } + + StartWorkersLocked(); + } + + QOpenGLContext* const PreviousContext = QOpenGLContext::currentContext(); + QSurface* const PreviousSurface = PreviousContext ? PreviousContext->surface() : nullptr; + + for (const std::shared_ptr& RequestedSynth : Required) + { + while (!RequestedSynth->Terminal) + { + ProcessCompletions(); + + if (RequestedSynth->Terminal) + break; + + QMutexLocker QueueLock(&mQueueMutex); + + if (mStopping || RequestedSynth->Terminal) + break; + + WaitForResultsLocked(); + } + } + + if (PreviousContext && PreviousSurface && (QOpenGLContext::currentContext() != PreviousContext || PreviousContext->surface() != PreviousSurface)) + PreviousContext->makeCurrent(PreviousSurface); + + for (lcPiece* Piece : Pieces) + if (Piece->mPieceInfo && Piece->mPieceInfo->GetSynthInfo() && !Piece->HasGeneratedMesh()) + return false; + + return true; +} + bool lcAssetLoader::EnsureTextureReady(lcTexture* Texture) { Q_ASSERT(QThread::currentThread() == thread()); @@ -1442,82 +1235,6 @@ bool lcAssetLoader::EnsureTextureReady(lcTexture* Texture) return Texture->IsReady(); } -void lcAssetLoader::QueueModelPiece(PieceInfo* Info) -{ - Q_ASSERT(QThread::currentThread() == thread()); - Q_ASSERT(Info->IsModel()); - - if (Info->IsLoading()) - WaitForRequest(Info); - - Info->ReleaseMesh(); - mLibrary->ClearPieceLoadError(Info); - - if (!Info->GetModel()->HasDirectGeometry()) - { - Info->mState = lcPieceInfoState::Loaded; - emit mLibrary->AssetRequestsChanged(); - return; - } - - Info->mState = lcPieceInfoState::Unloaded; - - EnqueuePiece(Info, Priority::Visible); -} - -void lcAssetLoader::InvalidatePiece(PieceInfo* Info) -{ - Q_ASSERT(QThread::currentThread() == thread()); - - std::shared_ptr Removed; - bool ReleaseHold = false; - - { - QMutexLocker QueueLock(&mQueueMutex); - - const auto It = mRequests.find(Info); - - if (It != mRequests.end()) - { - Removed = It->second; - Removed->Obsolete = true; - Removed->Terminal = true; - Removed->Succeeded = false; - ReleaseHold = !Removed->Running; - mRequests.erase(It); - - const auto EraseRequest = [&Removed](auto& Queue) - { - Queue.erase(std::remove(Queue.begin(), Queue.end(), Removed), Queue.end()); - }; - - EraseRequest(mQueue); - EraseRequest(mPausedQueue); - } - - for (Notification& Pending : mNotifications) - if (Pending.RequestedPart->Info == Info) - Pending.RequestedPart->Obsolete = true; - - mResultReady.wakeAll(); - } - - if (Removed && !Removed->Running) - Removed->StagedMesh.reset(); - - Info->mState = lcPieceInfoState::Unloaded; - mLibrary->ClearPieceLoadError(Info); - - // Running work retains its load hold until its stale completion arrives. - if (ReleaseHold) - mLibrary->ReleasePieceLoadHold(Info); - - if (Removed) - CancelUnusedTextureRequests(); - - emit mLibrary->AssetRequestsChanged(); -} - void lcAssetLoader::WaitForLoadQueue() { Q_ASSERT(QThread::currentThread() == thread()); @@ -1551,6 +1268,296 @@ bool lcAssetLoader::HasPendingWork() return Pending; } +QString lcAssetLoader::GetSynthMeshError(const lcPiece* Piece) const +{ + const std::unordered_map::const_iterator It = mFailedSynthErrors.find(Piece); + + return It != mFailedSynthErrors.end() ? It->second : QString(); +} + +void lcAssetLoader::RebuildModelPiece(PieceInfo* Info) +{ + Q_ASSERT(QThread::currentThread() == thread()); + Q_ASSERT(Info->IsModel()); + + if (Info->IsLoading()) + WaitForRequest(Info); + + Info->ReleaseMesh(); + mLibrary->ClearPieceLoadError(Info); + + if (!Info->GetModel()->HasDirectGeometry()) + { + Info->mState = lcPieceInfoState::Loaded; + emit mLibrary->AssetRequestsChanged(); + return; + } + + Info->mState = lcPieceInfoState::Unloaded; + + EnqueuePiece(Info, Priority::Visible); +} + +void lcAssetLoader::InvalidatePiece(PieceInfo* Info) +{ + Q_ASSERT(QThread::currentThread() == thread()); + + std::shared_ptr Removed; + bool ReleaseHold = false; + + { + QMutexLocker QueueLock(&mQueueMutex); + + const auto It = mRequests.find(Info); + + if (It != mRequests.end()) + { + Removed = It->second; + Removed->Obsolete = true; + Removed->Terminal = true; + Removed->Succeeded = false; + ReleaseHold = !Removed->Running; + mRequests.erase(It); + + mQueue.erase(std::remove(mQueue.begin(), mQueue.end(), Removed), mQueue.end()); + } + + for (Notification& Pending : mNotifications) + if (Pending.RequestedPiece->Info == Info) + Pending.RequestedPiece->Obsolete = true; + + mResultReady.wakeAll(); + } + + if (Removed && !Removed->Running) + Removed->StagedMesh.reset(); + + Info->mState = lcPieceInfoState::Unloaded; + mLibrary->ClearPieceLoadError(Info); + + // Running work retains its load hold until its stale completion arrives. + if (ReleaseHold) + mLibrary->ReleasePieceLoadHold(Info); + + if (Removed) + CancelUnusedTextureRequests(); + + emit mLibrary->AssetRequestsChanged(); +} + +void lcAssetLoader::OnConsumerReleased(PieceInfo* Info) +{ + // Ordinary running builds finish privately; discard them if their last consumer is gone. + Q_ASSERT(QThread::currentThread() == thread()); + + if (mLibrary->HasPieceConsumers(Info)) + return; + + std::shared_ptr Removed; + { + QMutexLocker QueueLock(&mQueueMutex); + + const auto It = mRequests.find(Info); + + if (It == mRequests.end()) + return; + + const std::shared_ptr& RequestedPiece = It->second; + + if (RequestedPiece->Running) + return; + + RequestedPiece->Obsolete = true; + + Removed = RequestedPiece; + Removed->Terminal = true; + Removed->Succeeded = false; + + Info->mState = lcPieceInfoState::Cancelled; + + mRequests.erase(It); + + mQueue.erase(std::remove(mQueue.begin(), mQueue.end(), Removed), mQueue.end()); + + mResultReady.wakeAll(); + } + + Removed->StagedMesh.reset(); + + CancelUnusedTextureRequests(); + + mLibrary->ReleasePieceLoadHold(Info); +} + +void lcAssetLoader::CancelSynthMesh(lcPiece* Piece) +{ + // Remove queued work immediately; running synth phases notice cancellation cooperatively. + Q_ASSERT(QThread::currentThread() == thread()); + + mSynthPieces.erase(Piece); + mFailedSynthErrors.erase(Piece); + + for (SynthNotification& Pending : mSynthNotifications) + if (Pending.RequestedSynth->Piece == Piece) + Pending.RequestedSynth->Cancelled.store(true, std::memory_order_relaxed); + + std::shared_ptr Removed; + + { + QMutexLocker QueueLock(&mQueueMutex); + const SynthRequestMap::iterator It = mSynthRequests.find(Piece); + + if (It == mSynthRequests.end()) + return; + + Removed = It->second; + Removed->Cancelled.store(true, std::memory_order_relaxed); + Removed->Terminal = true; + mSynthRequests.erase(It); + + mSynthQueue.erase(std::remove(mSynthQueue.begin(), mSynthQueue.end(), Removed), mSynthQueue.end()); + mResultReady.wakeAll(); + } + + if (!Removed->Running) + { + Removed->StagedMesh.reset(); + CancelUnusedTextureRequests(); + } +} + +void lcAssetLoader::CancelUnusedTextureRequests() +{ + std::vector> Cancelled; + + { + QMutexLocker QueueLock(&mQueueMutex); + + for (auto It = mTextureRequests.begin(); It != mTextureRequests.end();) + { + const std::shared_ptr& RequestedTexture = It->second; + + // A queued request owns one reference; all others belong to consumers. + if (RequestedTexture->Running || RequestedTexture->Texture->GetRefCount() > 1) + { + ++It; + continue; + } + + mTextureQueue.erase(std::remove(mTextureQueue.begin(), mTextureQueue.end(), RequestedTexture), mTextureQueue.end()); + + Cancelled.push_back(RequestedTexture); + + It = mTextureRequests.erase(It); + } + + if (!Cancelled.empty()) + mResultReady.wakeAll(); + } + + for (const std::shared_ptr& RequestedTexture : Cancelled) + { + lcTexture* Texture = RequestedTexture->Texture; + + Texture->DiscardDecodedImage(); + Texture->mState = lcTextureState::Unrequested; + Texture->mLoadFailure = lcTextureLoadError::None; + Texture->mLoadFailureDetails.clear(); + + mLibrary->ReleaseTexture(Texture); + } +} + +void lcAssetLoader::ReloadSynthMeshes() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + const std::vector Pieces(mSynthPieces.begin(), mSynthPieces.end()); + + for (lcPiece* Piece : Pieces) + Piece->RefreshGeneratedMesh(); +} + +void lcAssetLoader::PauseQueuedWork() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + mQueueMutex.lock(); + // Jobs stay queued; workers stop taking them until resume. + mPaused = true; + mQueueMutex.unlock(); + + for (;;) + { + ProcessCompletions(); + + QMutexLocker QueueLock(&mQueueMutex); + + if (mActiveWorkers == 0 && mCompletions.empty()) + break; + + WaitForResultsLocked(); + } + + // A mesh awaiting textures was built with the previous stud settings. + // Rebuild it after the settings change instead of publishing stale geometry. + for (const PieceRequestMap::value_type& Entry : mRequests) + { + const std::shared_ptr& RequestedPiece = Entry.second; + + if (RequestedPiece->ConvertingMesh) + { + RequestedPiece->MeshData.reset(); + RequestedPiece->ColorTranslucency.clear(); + RequestedPiece->ConvertingMesh = false; + RequestedPiece->Info->mState = lcPieceInfoState::Queued; + } + + if (!RequestedPiece->StagedMesh) + continue; + + RequestedPiece->StagedMesh.reset(); + RequestedPiece->Info->mState = lcPieceInfoState::Queued; + RequestedPiece->Running = false; + mQueue.push_back(RequestedPiece); + } + + // Pending generated meshes are restarted after the new stud style is installed. + for (const SynthRequestMap::value_type& Entry : mSynthRequests) + { + const std::shared_ptr& RequestedSynth = Entry.second; + + if (!RequestedSynth->StagedMesh && !RequestedSynth->ConvertingMesh) + continue; + + RequestedSynth->MeshData.reset(); + RequestedSynth->StagedMesh.reset(); + RequestedSynth->ColorTranslucency.clear(); + RequestedSynth->ConvertingMesh = false; + + if (std::find(mSynthQueue.begin(), mSynthQueue.end(), RequestedSynth) == mSynthQueue.end()) + mSynthQueue.push_back(RequestedSynth); + } +} + +void lcAssetLoader::ResumeQueuedWork() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + QMutexLocker QueueLock(&mQueueMutex); + + for (const std::shared_ptr& RequestedPiece : mQueue) + { + const bool SkipCache = RequestedPiece->Source.SkipCache; + + RequestedPiece->Source = mLibrary->SnapshotPieceSource(RequestedPiece->Info); + RequestedPiece->Source.SkipCache = SkipCache; + } + + mPaused = false; + StartWorkersLocked(); +} + void lcAssetLoader::CancelAndDrain() { // Shutdown/reload joins obsolete workers and releases holds without requiring GL success. @@ -1559,7 +1566,7 @@ void lcAssetLoader::CancelAndDrain() const bool WasCancelled = mLibrary->mCancelLoading.exchange(true); mLibrary->mPrimitiveLoaded.wakeAll(); - std::deque> Cancelled; + std::deque> Cancelled; std::deque> CancelledTextures; { @@ -1568,11 +1575,8 @@ void lcAssetLoader::CancelAndDrain() mStopping = true; mGeneration++; mQueue.clear(); - mPausedQueue.clear(); mTextureQueue.clear(); - mPausedTextureQueue.clear(); mSynthQueue.clear(); - mPausedSynthQueue.clear(); mSynthPieces.clear(); mFailedSynthErrors.clear(); @@ -1624,8 +1628,8 @@ void lcAssetLoader::CancelAndDrain() mResultReady.wakeAll(); } - for (const std::shared_ptr& RequestedPart : Cancelled) - mLibrary->ReleasePieceLoadHold(RequestedPart->Info); + for (const std::shared_ptr& RequestedPiece : Cancelled) + mLibrary->ReleasePieceLoadHold(RequestedPiece->Info); for (const std::shared_ptr& RequestedTexture : CancelledTextures) mLibrary->ReleaseTexture(RequestedTexture->Texture); @@ -1638,7 +1642,7 @@ void lcAssetLoader::CancelAndDrain() mNotifications.pop_front(); - mLibrary->ReleasePieceLoadHold(Notification.RequestedPart->Info); + mLibrary->ReleasePieceLoadHold(Notification.RequestedPiece->Info); } while (!mSynthNotifications.empty()) @@ -1652,103 +1656,3 @@ void lcAssetLoader::CancelAndDrain() mLibrary->mCancelLoading.store(WasCancelled); } - -void lcAssetLoader::PauseQueuedWork() -{ - Q_ASSERT(QThread::currentThread() == thread()); - - mQueueMutex.lock(); - mPaused = true; - mPausedQueue.swap(mQueue); - mPausedTextureQueue.swap(mTextureQueue); - mPausedSynthQueue.swap(mSynthQueue); - mQueueMutex.unlock(); - - for (;;) - { - ProcessCompletions(); - - QMutexLocker QueueLock(&mQueueMutex); - - if (mActiveWorkers == 0 && mCompletions.empty()) - break; - - WaitForResultsLocked(); - } - - // A mesh awaiting textures was built with the previous stud settings. - // Rebuild it after the settings change instead of publishing stale geometry. - for (const PartRequestMap::value_type& Entry : mRequests) - { - const std::shared_ptr& RequestedPart = Entry.second; - - if (RequestedPart->ConvertingMesh) - { - RequestedPart->MeshData.reset(); - RequestedPart->ColorTranslucency.clear(); - RequestedPart->ConvertingMesh = false; - RequestedPart->Info->mState = lcPieceInfoState::Queued; - } - - if (!RequestedPart->StagedMesh) - continue; - - RequestedPart->StagedMesh.reset(); - RequestedPart->Info->mState = lcPieceInfoState::Queued; - RequestedPart->Running = false; - mPausedQueue.push_back(RequestedPart); - } - - // Pending generated meshes are restarted after the new stud style is installed. - for (const SynthRequestMap::value_type& Entry : mSynthRequests) - { - const std::shared_ptr& RequestedSynth = Entry.second; - - if (!RequestedSynth->StagedMesh && !RequestedSynth->ConvertingMesh) - continue; - - RequestedSynth->MeshData.reset(); - RequestedSynth->StagedMesh.reset(); - RequestedSynth->ColorTranslucency.clear(); - RequestedSynth->ConvertingMesh = false; - - if (std::find(mPausedSynthQueue.begin(), mPausedSynthQueue.end(), RequestedSynth) == mPausedSynthQueue.end()) - mPausedSynthQueue.push_back(RequestedSynth); - } -} - -void lcAssetLoader::ResumeQueuedWork() -{ - Q_ASSERT(QThread::currentThread() == thread()); - - QMutexLocker QueueLock(&mQueueMutex); - - mPaused = false; - - for (const std::shared_ptr& RequestedPart : mPausedQueue) - { - const bool SkipCache = RequestedPart->Source.SkipCache; - - RequestedPart->Source = mLibrary->SnapshotPieceSource(RequestedPart->Info); - RequestedPart->Source.SkipCache = SkipCache; - } - - mQueue.swap(mPausedQueue); - mTextureQueue.swap(mPausedTextureQueue); - mSynthQueue.swap(mPausedSynthQueue); - - StartWorkersLocked(); -} - -bool lcAssetLoader::event(QEvent* Event) -{ - if (Event->type() == lcAssetLoaderCompletionEvent) - { - ProcessCompletions(); - DispatchNotifications(); - - return true; - } - - return QObject::event(Event); -} diff --git a/common/lc_assetloader.h b/common/lc_assetloader.h index 4323e6aa..2f6dbdd7 100644 --- a/common/lc_assetloader.h +++ b/common/lc_assetloader.h @@ -14,7 +14,7 @@ class lcTexture; enum class lcTextureLoadError; // Copied on the UI thread so workers do not consult live models or project lookup. -struct lcPartSourceSnapshot +struct lcPieceSourceSnapshot { QString FileName; QString LibraryDirectory; @@ -31,14 +31,14 @@ struct lcPartSourceSnapshot }; // Private worker output; meshes and dependencies are published only on the UI thread. -struct lcPartBuildResult +struct lcPieceBuildResult { - lcPartBuildResult() = default; - ~lcPartBuildResult(); - lcPartBuildResult(lcPartBuildResult&&) = default; - lcPartBuildResult& operator=(lcPartBuildResult&&) = default; - lcPartBuildResult(const lcPartBuildResult&) = delete; - lcPartBuildResult& operator=(const lcPartBuildResult&) = delete; + lcPieceBuildResult() = default; + ~lcPieceBuildResult(); + lcPieceBuildResult(lcPieceBuildResult&&) = default; + lcPieceBuildResult& operator=(lcPieceBuildResult&&) = default; + lcPieceBuildResult(const lcPieceBuildResult&) = delete; + lcPieceBuildResult& operator=(const lcPieceBuildResult&) = delete; quint64 Generation = 0; std::unique_ptr MeshData; @@ -59,24 +59,35 @@ public: lcAssetLoader(lcPiecesLibrary* Library); ~lcAssetLoader() override; + // Start using a piece: acquire a reference and load it if needed. Release it with ReleasePieceInfo(). + // Visible raises its loading priority. Wait blocks until loading settles and returns whether it succeeded; + // without Wait, this returns true immediately, even if the piece is not ready yet. bool LoadPieceInfo(PieceInfo* Info, lcPieceLoadFlags Flags); - void QueuePiece(PieceInfo* Info, bool Priority); - bool EnsurePieceReady(PieceInfo* Info); - bool EnsurePiecesReady(const std::vector& Parts); + + // Queue a background build for a piece the caller already owns, without acquiring a consumer reference or waiting. + // Reuse any existing request, keeping its priority. + void QueuePiece(PieceInfo* Info); + + // Build this instance's generated mesh from its current control points in the background. + // Cancel any older request and keep the previous mesh visible while the replacement is built. void QueueSynthMesh(lcPiece* Piece); - void CancelSynthMesh(lcPiece* Piece); + + bool EnsurePieceReady(PieceInfo* Info); + bool EnsurePiecesReady(const std::vector& Pieces); bool EnsureSynthMeshesReady(const std::vector& Pieces); - QString GetSynthMeshError(const lcPiece* Piece) const; - void ReloadSynthMeshes(); - void QueueModelPiece(PieceInfo* Info); - void InvalidatePiece(PieceInfo* Info); bool EnsureTextureReady(lcTexture* Texture); void WaitForLoadQueue(); - void CancelAndDrain(); + bool HasPendingWork(); + QString GetSynthMeshError(const lcPiece* Piece) const; + + void RebuildModelPiece(PieceInfo* Info); + void InvalidatePiece(PieceInfo* Info); + void OnConsumerReleased(PieceInfo* Info); + void CancelSynthMesh(lcPiece* Piece); + void ReloadSynthMeshes(); void PauseQueuedWork(); void ResumeQueuedWork(); - void OnConsumerReleased(PieceInfo* Info); - bool HasPendingWork(); + void CancelAndDrain(); private: enum class Priority @@ -89,11 +100,11 @@ private: // Owns one PieceInfo load hold through completion and deferred notification. // Parsing and conversion are separate worker phases; textures settle before publication. - struct Request + struct PieceRequest { PieceInfo* Info; quint64 Generation; - lcPartSourceSnapshot Source; + lcPieceSourceSnapshot Source; Priority LoadPriority; qint64 EnqueuedAt; bool Running = false; // A worker phase is outstanding, including its completion. @@ -144,15 +155,15 @@ private: QString Error; }; - using PartRequestMap = std::map>; + using PieceRequestMap = std::map>; using TextureRequestMap = std::map>; using SynthRequestMap = std::map>; // Workers hand off one private result here; no live asset is published by a worker. struct Completion { - std::shared_ptr RequestedPart; - lcPartBuildResult Result; + std::shared_ptr RequestedPiece; + lcPieceBuildResult Result; std::shared_ptr RequestedTexture; lcTextureBuildResult TextureResult; std::shared_ptr RequestedSynth; @@ -163,7 +174,7 @@ private: // UI-thread delivery retains the load hold after publication, until callbacks finish. struct Notification { - std::shared_ptr RequestedPart; + std::shared_ptr RequestedPiece; bool Loaded; QString Error; }; @@ -175,28 +186,41 @@ private: PieceInfo* Info; }; + // Enqueue and promote requests. Workers read copied sources and private build data. // Acquires mQueueMutex internally; callers must not already hold it. void EnqueuePiece(PieceInfo* Info, Priority LoadPriority); // Caller holds mQueueMutex. - void PromoteRequestPriorityLocked(const std::shared_ptr& RequestedPart, Priority LoadPriority); + void PromoteRequestPriorityLocked(const std::shared_ptr& RequestedPiece, Priority LoadPriority); void QueueTexture(lcTexture* Texture, Priority LoadPriority); - static QString TextureFailureMessage(const lcTexture* Texture); - void CancelUnusedTextureRequests(); // Caller holds mQueueMutex. void StartWorkersLocked(); - void LoadQueuedPieces(); + void RunWorker(); + + // UI-thread completion handling: colors, conversion, texture dependencies, publication. bool ProcessCompletions(); - void DispatchNotifications(); - void FinishPart(const std::shared_ptr& RequestedPart); - void CheckWaitingParts(); - void CheckWaitingSynths(); - void FinishSynth(const std::shared_ptr& RequestedSynth); + void CompletePiece(Completion& Completed); + void CompleteSynth(Completion& Completed); + void CompleteTexture(Completion& Completed); + static void ResolveMeshColors(lcLibraryMeshData& MeshData, std::vector& ColorTranslucency, int& DefaultColorIndex); + bool ResolveAndQueueMeshTextures(lcMesh& Mesh, const std::vector& SearchDirectories, + const std::map>& EmbeddedData, Priority LoadPriority, QString& Error); + static bool MeshTexturesSettled(const lcMesh& Mesh, QString& Error); + static QString TextureFailureMessage(const lcTexture* Texture); void UploadTextureRequest(const std::shared_ptr& RequestedTexture); void FinishTextureRequest(const std::shared_ptr& RequestedTexture); + void FinishPiecesWithSettledTextures(); + void FinishSynthsWithSettledTextures(); + void FinishPiece(const std::shared_ptr& RequestedPiece); + void FinishSynth(const std::shared_ptr& RequestedSynth); + + // Notify only from the event handler, after synchronous waits have returned. + void DispatchNotifications(); + bool event(QEvent* Event) override; // Caller holds mQueueMutex; the condition wait releases it and reacquires it on return. void WaitForResultsLocked(); bool WaitForRequest(PieceInfo* Info); - bool event(QEvent* Event) override; + + void CancelUnusedTextureRequests(); lcPiecesLibrary* const mLibrary; // Protects scheduling maps/queues, worker counts, and result handoff. @@ -205,20 +229,18 @@ private: QWaitCondition mResultReady; // Serializes full queue drains; scoped readiness waits do not acquire this mutex. QMutex mDrainMutex; - PartRequestMap mRequests; + PieceRequestMap mRequests; TextureRequestMap mTextureRequests; SynthRequestMap mSynthRequests; // UI-thread diagnostics and instance tracking; these do not own the pieces. std::unordered_map mFailedSynthErrors; std::unordered_set mSynthPieces; - std::deque> mQueue; - std::deque> mPausedQueue; + // Pending worker phases remain here while paused; mPaused prevents dispatch. + std::deque> mQueue; std::deque> mTextureQueue; - std::deque> mPausedTextureQueue; std::deque> mSynthQueue; - std::deque> mPausedSynthQueue; std::deque mCompletions; - std::deque> mStarted; + std::deque> mStarted; std::deque> mStartedTextures; // UI-thread only; public signals are deferred until outside synchronous waits. std::deque mNotifications; diff --git a/common/lc_library.cpp b/common/lc_library.cpp index 81e6f39c..c7f3796a 100644 --- a/common/lc_library.cpp +++ b/common/lc_library.cpp @@ -1709,9 +1709,9 @@ QString lcPiecesLibrary::GetSynthMeshError(const lcPiece* Piece) const return mAssetLoader ? mAssetLoader->GetSynthMeshError(Piece) : QString(); } -void lcPiecesLibrary::QueueModelPiece(PieceInfo* Info) +void lcPiecesLibrary::RebuildModelPiece(PieceInfo* Info) { - mAssetLoader->QueueModelPiece(Info); + mAssetLoader->RebuildModelPiece(Info); } void lcPiecesLibrary::InvalidatePiece(PieceInfo* Info) @@ -1803,9 +1803,9 @@ void lcPiecesLibrary::WaitForLoadQueue() mAssetLoader->WaitForLoadQueue(); } -lcPartSourceSnapshot lcPiecesLibrary::SnapshotPieceSource(const PieceInfo* Info) const +lcPieceSourceSnapshot lcPiecesLibrary::SnapshotPieceSource(const PieceInfo* Info) const { - lcPartSourceSnapshot Source; + lcPieceSourceSnapshot Source; Source.FileName = QString::fromLatin1(Info->mFileName); Source.LibraryDirectory = mLibraryDir.absolutePath(); @@ -1830,9 +1830,9 @@ lcPartSourceSnapshot lcPiecesLibrary::SnapshotPieceSource(const PieceInfo* Info) return Source; } -lcPartBuildResult lcPiecesLibrary::BuildPieceData(const lcPartSourceSnapshot& Source) +lcPieceBuildResult lcPiecesLibrary::BuildPieceData(const lcPieceSourceSnapshot& Source) { - lcPartBuildResult Result; + lcPieceBuildResult Result; if (mCancelLoading.load()) { @@ -1953,7 +1953,7 @@ lcPartBuildResult lcPiecesLibrary::BuildPieceData(const lcPartSourceSnapshot& So return Result; } -void lcPiecesLibrary::SaveBuiltPieceCache(const lcPartSourceSnapshot& Source, lcMesh& Mesh) +void lcPiecesLibrary::SaveBuiltPieceCache(const lcPieceSourceSnapshot& Source, lcMesh& Mesh) { if (Source.ZipFileType == lcZipFileType::Count) return; @@ -2332,12 +2332,12 @@ void lcPiecesLibrary::SetStudStyle(lcStudStyle StudStyle, bool Reload, bool Stud Info->ReleaseMesh(); Info->mState = lcPieceInfoState::Unloaded; - mAssetLoader->QueuePiece(Info, false); + mAssetLoader->QueuePiece(Info); } else if (!Info->IsProject()) { Info->Unload(); - mAssetLoader->QueuePiece(Info, false); + mAssetLoader->QueuePiece(Info); } mBuffersDirty = true; diff --git a/common/lc_library.h b/common/lc_library.h index 0149f24a..b478448c 100644 --- a/common/lc_library.h +++ b/common/lc_library.h @@ -16,8 +16,8 @@ class lcLibraryMeshData; class lcThumbnailManager; class lcAssetLoader; class lcTextureManager; -struct lcPartSourceSnapshot; -struct lcPartBuildResult; +struct lcPieceSourceSnapshot; +struct lcPieceBuildResult; struct lcEmbeddedData; struct lcTextureBuildResult; @@ -206,7 +206,7 @@ public: // Caller keeps the instances alive through the wait; false means missing generated geometry. bool EnsureSynthMeshesReady(const std::vector& Pieces); QString GetSynthMeshError(const lcPiece* Piece) const; - void QueueModelPiece(PieceInfo* Info); + void RebuildModelPiece(PieceInfo* Info); // Obsoletes pending work and resets readiness without acquiring a reference. void InvalidatePiece(PieceInfo* Info); // Waits through GPU upload; caller retains the texture, and false means not ready. @@ -223,10 +223,10 @@ public: QString GetPieceLoadError(const PieceInfo* Info) const; void ClearPieceLoadError(const PieceInfo* Info); bool LoadBuiltinPieces(); - lcPartSourceSnapshot SnapshotPieceSource(const PieceInfo* Info) const; + lcPieceSourceSnapshot SnapshotPieceSource(const PieceInfo* Info) const; // Worker build service: returns private CPU data/errors without publishing asset state. - lcPartBuildResult BuildPieceData(const lcPartSourceSnapshot& Source); - void SaveBuiltPieceCache(const lcPartSourceSnapshot& Source, lcMesh& Mesh); + lcPieceBuildResult BuildPieceData(const lcPieceSourceSnapshot& Source); + void SaveBuiltPieceCache(const lcPieceSourceSnapshot& Source, lcMesh& Mesh); // UI-thread drain of all work, including unrelated assets; not a success check. void WaitForLoadQueue(); diff --git a/common/lc_model.cpp b/common/lc_model.cpp index b256c07b..91cc366b 100644 --- a/common/lc_model.cpp +++ b/common/lc_model.cpp @@ -434,7 +434,7 @@ bool lcModel::RebindPieceInfo(PieceInfo* Previous, PieceInfo* Replacement) void lcModel::QueueMeshBuild() { mPieceInfo->SetModel(this); - lcGetPiecesLibrary()->QueueModelPiece(mPieceInfo); + lcGetPiecesLibrary()->RebuildModelPiece(mPieceInfo); } void lcModel::UpdateAllViews() const diff --git a/docs/asset-loading.md b/docs/asset-loading.md index 99adf50a..553b9235 100644 --- a/docs/asset-loading.md +++ b/docs/asset-loading.md @@ -275,6 +275,14 @@ caller omits unavailable geometry, while the loading indicator exposes errors. ## A request from acquisition to notification +The loader implementation follows this lifecycle: request acquisition and queueing, +worker scheduling and `RunWorker()`, UI-thread `ProcessCompletions()` dispatch to +`CompletePiece()`, `CompleteSynth()`, or `CompleteTexture()`, dependency settlement +and publication, then deferred notifications. Readiness waits follow these paths; +invalidation, pause/resume, and shutdown are grouped at the end of the file. +Piece and synth handlers share color preparation and texture dependency checks, +while keeping their distinct ownership and publication rules explicit. + ```mermaid sequenceDiagram participant Caller as Caller / UI thread @@ -503,6 +511,10 @@ open discards the candidate. A successful switch releases old owners, cancels old-only queued work, and preserves shared asset identities and requests. Neither path demotes request priorities. +Pending jobs remain in the normal piece, texture, and synth queues while paused; +`mPaused` prevents dispatch. Resume refreshes queued piece source snapshots before +allowing workers to take jobs again. + Stud-style changes pause dispatch, finish running phases, discard staging built with old settings, refresh sources, and rebuild affected published meshes and generated geometry. The existing style-change path resumes and drains work.