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.