From 4129788f9576a57783749c12ce9926ae9a61603a Mon Sep 17 00:00:00 2001 From: Leonardo Zide Date: Thu, 15 Oct 2026 14:37:07 -0700 Subject: [PATCH] Build piece and synth meshes in one worker job Color finalization no longer requires a UI-thread handoff between parsing and mesh conversion. Have the library and synth build services return converted private meshes, removing intermediate request/result mesh data and conversion requeue paths. Fall back to source parsing immediately when a cached mesh cannot be decoded, eliminating SkipCache and the UI retry branch. Simplify paused staging cleanup and document the combined worker lifecycle; section finalization and publication remain on the UI thread. --- common/lc_assetloader.cpp | 90 +++---------------------- common/lc_assetloader.h | 20 +++--- common/lc_library.cpp | 138 +++++++++++++++++++------------------- common/lc_library.h | 2 +- common/lc_synth.cpp | 16 ++--- common/lc_synth.h | 3 +- docs/asset-loading.md | 21 +++--- 7 files changed, 107 insertions(+), 183 deletions(-) diff --git a/common/lc_assetloader.cpp b/common/lc_assetloader.cpp index dcc47276..4613b857 100644 --- a/common/lc_assetloader.cpp +++ b/common/lc_assetloader.cpp @@ -14,7 +14,7 @@ constexpr QEvent::Type lcAssetLoaderCompletionEvent = static_cast( lcPieceBuildResult::~lcPieceBuildResult() = default; -// Normal part flow: enqueue -> build -> resolve colors -> convert -> settle textures -> publish -> notify. +// Normal part flow: enqueue -> build and convert -> finalize sections -> settle textures -> publish -> notify. // Workers own private build data. Publication and deferred notifications stay on the UI thread. lcAssetLoader::lcAssetLoader(lcPiecesLibrary* Library) : mLibrary(Library) @@ -265,16 +265,7 @@ void lcAssetLoader::RunWorker() { Completed.RequestedPiece = RequestedPiece; - if (RequestedPiece->MeshData) - { - Completed.PieceResult.Mesh.reset(RequestedPiece->MeshData->CreateMesh(nullptr)); - RequestedPiece->MeshData.reset(); - - if (!Completed.PieceResult.Mesh) - Completed.PieceResult.Error = tr("Could not prepare piece %1 for rendering.").arg(RequestedPiece->Source.FileName); - } - else - Completed.PieceResult = mLibrary->BuildPieceData(RequestedPiece->Source); + Completed.PieceResult = mLibrary->BuildPieceData(RequestedPiece->Source); } else if (RequestedTexture) { @@ -285,15 +276,7 @@ void lcAssetLoader::RunWorker() { Completed.RequestedSynth = RequestedSynth; - if (RequestedSynth->Cancelled.load(std::memory_order_relaxed)) - RequestedSynth->MeshData.reset(); - else if (RequestedSynth->MeshData) - { - Completed.SynthMesh.reset(RequestedSynth->MeshData->CreateMesh(&RequestedSynth->Cancelled)); - RequestedSynth->MeshData.reset(); - } - else - Completed.SynthMeshData = RequestedSynth->SynthInfo->BuildMeshData(RequestedSynth->ControlPoints, RequestedSynth->Cancelled); + Completed.SynthMesh = RequestedSynth->SynthInfo->BuildMesh(RequestedSynth->ControlPoints, RequestedSynth->Cancelled); } { @@ -459,42 +442,13 @@ void lcAssetLoader::ProcessPieceCompletion(Completion& Completed) RequestedPiece->Error = Completed.PieceResult.Error; RequestedPiece->EmptyGeometry = Completed.PieceResult.EmptyGeometry; - if (RequestedPiece->Error.isEmpty()) + if (RequestedPiece->Error.isEmpty() && Completed.PieceResult.Mesh) { - if (Completed.PieceResult.FromCache && !Completed.PieceResult.Mesh) - { - // Rebuild a corrupt cache privately on a worker, then use the normal commit path. - RequestedPiece->Source.SkipCache = true; - Info->mState = lcPieceInfoState::Queued; + RequestedPiece->StagedMesh = std::move(Completed.PieceResult.Mesh); - QMutexLocker QueueLock(&mQueueMutex); + RequestedPiece->StagedMesh->FinalizeSections(); - mPieceQueue.push_back(RequestedPiece); - StartWorkersLocked(); - - return; - } - else if (Completed.PieceResult.MeshData) - { - // Queue the expensive geometry conversion; colors are finalized after it completes. - RequestedPiece->MeshData = std::move(Completed.PieceResult.MeshData); - Info->mState = lcPieceInfoState::Queued; - - QMutexLocker QueueLock(&mQueueMutex); - - mPieceQueue.push_back(RequestedPiece); - StartWorkersLocked(); - - return; - } - else if (Completed.PieceResult.Mesh) - { - RequestedPiece->StagedMesh = std::move(Completed.PieceResult.Mesh); - - RequestedPiece->StagedMesh->FinalizeSections(); - - RequestedPiece->SaveCache = !Completed.PieceResult.FromCache; - } + RequestedPiece->SaveCache = !Completed.PieceResult.FromCache; } // A private mesh cannot be published until all its declared textures settle. @@ -535,19 +489,6 @@ void lcAssetLoader::ProcessSynthCompletion(Completion& Completed) return; } - if (Completed.SynthMeshData) - { - // Queue the expensive geometry conversion; colors are finalized after it completes. - RequestedSynth->MeshData = std::move(Completed.SynthMeshData); - - 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)); @@ -1314,7 +1255,7 @@ void lcAssetLoader::CancelPieceLoad(PieceInfo* Info) void lcAssetLoader::CancelSynthMesh(lcPiece* Piece) { - // Remove queued work immediately; running synth phases notice cancellation cooperatively. + // Remove queued work immediately; running synth builds notice cancellation cooperatively. Q_ASSERT(QThread::currentThread() == thread()); mSynthPieces.erase(Piece); @@ -1428,18 +1369,11 @@ void lcAssetLoader::PauseQueuedWork() { const std::shared_ptr& RequestedPiece = Entry.second; - if (RequestedPiece->MeshData) - { - RequestedPiece->MeshData.reset(); - RequestedPiece->Info->mState = lcPieceInfoState::Queued; - } - if (!RequestedPiece->StagedMesh) continue; RequestedPiece->StagedMesh.reset(); RequestedPiece->Info->mState = lcPieceInfoState::Queued; - RequestedPiece->AwaitingCompletion = false; mPieceQueue.push_back(RequestedPiece); } @@ -1448,10 +1382,9 @@ void lcAssetLoader::PauseQueuedWork() { const std::shared_ptr& RequestedSynth = Entry.second; - if (!RequestedSynth->StagedMesh && !RequestedSynth->MeshData) + if (!RequestedSynth->StagedMesh) continue; - RequestedSynth->MeshData.reset(); RequestedSynth->StagedMesh.reset(); if (std::find(mSynthQueue.begin(), mSynthQueue.end(), RequestedSynth) == mSynthQueue.end()) @@ -1466,12 +1399,7 @@ void lcAssetLoader::ResumeQueuedWork() QMutexLocker QueueLock(&mQueueMutex); for (const std::shared_ptr& RequestedPiece : mPieceQueue) - { - const bool SkipCache = RequestedPiece->Source.SkipCache; - RequestedPiece->Source = mLibrary->SnapshotPieceSource(RequestedPiece->Info); - RequestedPiece->Source.SkipCache = SkipCache; - } mPaused = false; StartWorkersLocked(); diff --git a/common/lc_assetloader.h b/common/lc_assetloader.h index dbe000be..2eeeeac3 100644 --- a/common/lc_assetloader.h +++ b/common/lc_assetloader.h @@ -8,7 +8,6 @@ class PieceInfo; class lcPiece; class lcMesh; class lcSynthInfo; -class lcLibraryMeshData; class lcTexture; enum class lcTextureLoadError; @@ -22,7 +21,6 @@ struct lcPieceSourceSnapshot std::map> EmbeddedData; QStringList InlineMeshLines; bool InlineModel = false; - bool SkipCache = false; lcZipFileType ZipFileType = lcZipFileType::Count; int ZipFileIndex = -1; lcStudStyle StudStyle = lcStudStyle::Plain; @@ -40,7 +38,6 @@ struct lcPieceBuildResult lcPieceBuildResult(const lcPieceBuildResult&) = delete; lcPieceBuildResult& operator=(const lcPieceBuildResult&) = delete; - std::unique_ptr MeshData; std::unique_ptr Mesh; bool FromCache = false; bool EmptyGeometry = false; @@ -100,18 +97,17 @@ public: private: // Owns one PieceInfo load hold through completion and deferred notification. - // Parsing and conversion are separate worker phases; textures settle before publication. + // One worker job builds and converts geometry; textures settle before publication. struct PieceRequest { PieceInfo* Info; lcPieceSourceSnapshot Source; lcPieceLoadPriority LoadPriority; qint64 EnqueuedAt; - bool AwaitingCompletion = false; // A worker phase is outstanding, including its completion. + bool AwaitingCompletion = false; // A worker job is outstanding, including its completion. bool Obsolete = false; // Its result must not publish, even if the build succeeds. bool Terminal = false; // The wait has settled; this does not imply success. bool Succeeded = false; // Published successfully, including valid empty geometry. - std::unique_ptr MeshData; // Present when the next worker phase is mesh conversion. std::unique_ptr StagedMesh; bool EmptyGeometry = false; bool SaveCache = false; @@ -142,7 +138,6 @@ private: bool AwaitingCompletion = false; std::atomic_bool Cancelled = false; bool Terminal = false; - std::unique_ptr MeshData; // Present when the next worker phase is mesh conversion. std::unique_ptr StagedMesh; QString Error; }; @@ -159,7 +154,6 @@ private: std::shared_ptr RequestedTexture; lcTextureBuildResult TextureResult; std::shared_ptr RequestedSynth; - std::unique_ptr SynthMeshData; std::unique_ptr SynthMesh; }; @@ -187,7 +181,7 @@ private: void StartWorkersLocked(); void RunWorker(); - // UI-thread completion handling: colors, conversion, texture dependencies, publication. + // UI-thread completion handling: colors, texture dependencies, publication. bool ProcessCompletions(); void ProcessPieceCompletion(Completion& Completed); void ProcessSynthCompletion(Completion& Completed); @@ -214,24 +208,30 @@ private: void CancelAndDrain(); lcPiecesLibrary* const mLibrary; + // Protects scheduling maps/queues, worker counts, and result handoff. // Private build data passes between worker and UI phases through completions. QMutex mQueueMutex; QWaitCondition mResultReady; + PieceRequestMap mPieceRequests; TextureRequestMap mTextureRequests; SynthRequestMap mSynthRequests; + // UI-thread diagnostics and instance tracking; these do not own the pieces. std::unordered_map mSynthErrors; std::unordered_set mSynthPieces; - // Pending worker phases remain here while paused; mPaused prevents dispatch. + std::deque> mPieceQueue; std::deque> mTextureQueue; std::deque> mSynthQueue; + std::deque mCompletions; + // UI-thread only; public signals are deferred until outside synchronous waits. std::deque mPieceNotifications; std::deque mSynthNotifications; + std::vector> mFutures; int mActiveWorkers = 0; bool mPaused = false; diff --git a/common/lc_library.cpp b/common/lc_library.cpp index e8ab6112..8cb97306 100644 --- a/common/lc_library.cpp +++ b/common/lc_library.cpp @@ -1823,10 +1823,34 @@ lcPieceBuildResult lcPiecesLibrary::BuildPieceData(const lcPieceSourceSnapshot& return Result; } + if (!Source.InlineModel && Source.ZipFileType != lcZipFileType::Count && mZipFiles[static_cast(Source.ZipFileType)]) + { + lcMemFile CacheData; + + if (ReadArchiveCacheFile(Source.CachePath, CacheData)) + { + qint32 Flags; + + if (CacheData.ReadBuffer(&Flags, sizeof(Flags)) == sizeof(Flags) && Flags == MeshCacheSettingsKey(Source.StudStyle, Source.StudCylinderColorEnabled)) + { + Result.Mesh.reset(new lcMesh); + + if (Result.Mesh->FileLoad(CacheData)) + { + Result.FromCache = true; + return Result; + } + + Result.Mesh.reset(); + } + } + } + + lcLibraryMeshData MeshData; + lcMeshLoader MeshLoader(MeshData, nullptr, lcMeshLoaderFlag::Optimize | lcMeshLoaderFlag::RequireAllIncludes); + if (Source.InlineModel) { - Result.MeshData.reset(new lcLibraryMeshData); - lcMemFile PieceFile; for (const QString& Line : Source.InlineMeshLines) @@ -1839,97 +1863,71 @@ lcPieceBuildResult lcPiecesLibrary::BuildPieceData(const lcPieceSourceSnapshot& PieceFile.Seek(0, SEEK_SET); - lcMeshLoader MeshLoader(*Result.MeshData, nullptr, lcMeshLoaderFlag::Optimize | lcMeshLoaderFlag::RequireAllIncludes); - if (!MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED)) { - Result.MeshData.reset(); Result.Error = tr("Could not load model geometry %1.").arg(Source.FileName); if (!MeshLoader.GetError().isEmpty()) Result.Error += QLatin1Char('\n') + MeshLoader.GetError(); + + return Result; } - else if (Result.MeshData->IsEmpty()) + + if (MeshData.IsEmpty()) { - Result.MeshData.reset(); Result.EmptyGeometry = true; + return Result; } - - if (Result.MeshData) - Result.MeshData->SetMeshLoader(nullptr); - - return Result; - } - - if (!Source.SkipCache && Source.ZipFileType != lcZipFileType::Count && mZipFiles[static_cast(Source.ZipFileType)]) - { - lcMemFile CacheData; - - if (ReadArchiveCacheFile(Source.CachePath, CacheData)) - { - qint32 Flags; - - if (CacheData.ReadBuffer(&Flags, sizeof(Flags)) == sizeof(Flags) && Flags == MeshCacheSettingsKey(Source.StudStyle, Source.StudCylinderColorEnabled)) - { - Result.FromCache = true; - Result.Mesh.reset(new lcMesh); - - if (!Result.Mesh->FileLoad(CacheData)) - Result.Mesh.reset(); - - return Result; - } - } - } - - Result.MeshData.reset(new lcLibraryMeshData); - - lcMeshLoader MeshLoader(*Result.MeshData, nullptr, lcMeshLoaderFlag::Optimize | lcMeshLoaderFlag::RequireAllIncludes); - - bool Loaded = false; - - if (Source.ZipFileType != lcZipFileType::Count && mZipFiles[static_cast(Source.ZipFileType)]) - { - lcMemFile PieceFile; - - if (mZipFiles[static_cast(Source.ZipFileType)]->ExtractFile(Source.ZipFileIndex, PieceFile)) - Loaded = MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED); } else { - lcDiskFile PieceFile; + bool Loaded = false; - PieceFile.SetFileName(QDir(Source.LibraryDirectory).absoluteFilePath(QStringLiteral("parts/") + Source.FileName)); - - if (PieceFile.Open(QIODevice::ReadOnly)) - Loaded = MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED); - - if (mHasUnofficial && !Loaded) + if (Source.ZipFileType != lcZipFileType::Count && mZipFiles[static_cast(Source.ZipFileType)]) { - Result.MeshData->Clear(); + lcMemFile PieceFile; - PieceFile.SetFileName(QDir(Source.LibraryDirectory).absoluteFilePath(QStringLiteral("unofficial/parts/") + Source.FileName)); + if (mZipFiles[static_cast(Source.ZipFileType)]->ExtractFile(Source.ZipFileIndex, PieceFile)) + Loaded = MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED); + } + else + { + lcDiskFile PieceFile; + + PieceFile.SetFileName(QDir(Source.LibraryDirectory).absoluteFilePath(QStringLiteral("parts/") + Source.FileName)); if (PieceFile.Open(QIODevice::ReadOnly)) Loaded = MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED); + + if (mHasUnofficial && !Loaded) + { + MeshData.Clear(); + + PieceFile.SetFileName(QDir(Source.LibraryDirectory).absoluteFilePath(QStringLiteral("unofficial/parts/") + Source.FileName)); + + if (PieceFile.Open(QIODevice::ReadOnly)) + Loaded = MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED); + } } + + if (mCancelLoading.load()) + Result.Error = tr("Library loading was cancelled."); + else if (!Loaded) + { + Result.Error = tr("Could not load part %1.").arg(Source.FileName); + + if (!MeshLoader.GetError().isEmpty()) + Result.Error += QLatin1Char('\n') + MeshLoader.GetError(); + } + + if (!Result.Error.isEmpty()) + return Result; } - if (mCancelLoading.load()) - Result.Error = tr("Library loading was cancelled."); - else if (!Loaded) - { - Result.Error = tr("Could not load part %1.").arg(Source.FileName); + Result.Mesh.reset(MeshData.CreateMesh(nullptr)); - if (!MeshLoader.GetError().isEmpty()) - Result.Error += QLatin1Char('\n') + MeshLoader.GetError(); - } - - if (!Result.Error.isEmpty()) - Result.MeshData.reset(); - - if (Result.MeshData) - Result.MeshData->SetMeshLoader(nullptr); + if (!Result.Mesh) + Result.Error = tr("Could not prepare piece %1 for rendering.").arg(Source.FileName); return Result; } diff --git a/common/lc_library.h b/common/lc_library.h index a3898d54..855cd4e7 100644 --- a/common/lc_library.h +++ b/common/lc_library.h @@ -222,7 +222,7 @@ public: void ClearPieceLoadError(const PieceInfo* Info); bool LoadBuiltinPieces(); lcPieceSourceSnapshot SnapshotPieceSource(const PieceInfo* Info) const; - // Worker build service: returns private CPU data/errors without publishing asset state. + // Worker build service: returns a converted private mesh or an error without publishing asset state. lcPieceBuildResult BuildPieceData(const lcPieceSourceSnapshot& Source); void SaveBuiltPieceCache(const lcPieceSourceSnapshot& Source, lcMesh& Mesh); diff --git a/common/lc_synth.cpp b/common/lc_synth.cpp index 66a170a2..54a0e606 100644 --- a/common/lc_synth.cpp +++ b/common/lc_synth.cpp @@ -4,6 +4,7 @@ #include "lc_application.h" #include "lc_file.h" #include "lc_meshloader.h" +#include "lc_mesh.h" #include "pieceinf.h" #include @@ -1591,7 +1592,7 @@ void lcSynthInfoUniversalJoint::AddParts(lcMemFile& File, lcLibraryMeshData&, co File.WriteBuffer(Line, strlen(Line)); } -std::unique_ptr lcSynthInfo::BuildMeshData(const std::vector& ControlPoints, const std::atomic_bool& Cancelled) const +std::unique_ptr lcSynthInfo::BuildMesh(const std::vector& ControlPoints, const std::atomic_bool& Cancelled) const { if (Cancelled.load(std::memory_order_relaxed)) return nullptr; @@ -1603,10 +1604,10 @@ std::unique_ptr lcSynthInfo::BuildMeshData(const std::vector< if (Cancelled.load(std::memory_order_relaxed)) return nullptr; - std::unique_ptr MeshData(new lcLibraryMeshData); + lcLibraryMeshData MeshData; lcMemFile File; // todo: rewrite this to pass the parts directly - AddParts(File, *MeshData, Sections, &Cancelled); + AddParts(File, MeshData, Sections, &Cancelled); if (Cancelled.load(std::memory_order_relaxed)) return nullptr; @@ -1614,14 +1615,11 @@ std::unique_ptr lcSynthInfo::BuildMeshData(const std::vector< File.WriteU8(0); File.Seek(0, SEEK_SET); - lcMeshLoader MeshLoader(*MeshData, nullptr, lcMeshLoaderFlag::RequireAllIncludes); + lcMeshLoader MeshLoader(MeshData, nullptr, lcMeshLoaderFlag::RequireAllIncludes); MeshLoader.SetCancellationFlag(&Cancelled); - if (MeshLoader.LoadMesh(File, LC_MESHDATA_SHARED) && !MeshData->IsEmpty() && !Cancelled.load(std::memory_order_relaxed)) - { - MeshData->SetMeshLoader(nullptr); - return MeshData; - } + if (MeshLoader.LoadMesh(File, LC_MESHDATA_SHARED) && !MeshData.IsEmpty() && !Cancelled.load(std::memory_order_relaxed)) + return std::unique_ptr(MeshData.CreateMesh(&Cancelled)); return nullptr; } diff --git a/common/lc_synth.h b/common/lc_synth.h index 55c7a3fb..b93ccd35 100644 --- a/common/lc_synth.h +++ b/common/lc_synth.h @@ -4,6 +4,7 @@ #include "piece.h" class lcLibraryMeshData; +class lcMesh; class lcSynthInfo { @@ -39,7 +40,7 @@ public: virtual void GetDefaultControlPoints(std::vector& ControlPoints) const = 0; virtual void VerifyControlPoints(std::vector& ControlPoints) const = 0; int InsertControlPoint(std::vector& ControlPoints, const lcVector3& Start, const lcVector3& End) const; - std::unique_ptr BuildMeshData(const std::vector& ControlPoints, const std::atomic_bool& Cancelled) const; + std::unique_ptr BuildMesh(const std::vector& ControlPoints, const std::atomic_bool& Cancelled) const; protected: using SectionCallbackFunc = std::function; diff --git a/docs/asset-loading.md b/docs/asset-loading.md index 43ce342c..3ce35d97 100644 --- a/docs/asset-loading.md +++ b/docs/asset-loading.md @@ -229,13 +229,13 @@ flowchart LR end ``` -The diagram shows the normal paths. Mesh parsing and conversion are separate -worker phases, so a part can return to `Queued` between phases. Invalidation or -unloading resets readiness; cancellation and stale-result disposal also have -request-level terminal flags, independent of the published asset state. -Queued piece and synth requests use the presence of `MeshData` to select -conversion; an empty pointer selects parsing or generation. `AwaitingCompletion` tracks -outstanding worker phases through completion processing. +The diagram shows the normal paths. Parsing or synth generation and geometry +conversion run consecutively in one worker job. Intermediate `lcLibraryMeshData` +stays local to the library's `BuildPieceData()` or the synth definition's `BuildMesh()`; +both return a private converted mesh for UI-thread finalization. +Invalidation or unloading resets readiness; cancellation and stale-result disposal +also have request-level terminal flags, independent of the published asset state. +`AwaitingCompletion` tracks an outstanding worker job through completion processing. A part becomes `Loaded` after its private mesh is committed and all declared textures are ready. Valid empty geometry may also settle successfully. A missing @@ -296,8 +296,7 @@ sequenceDiagram Library->>Library: Acquire consumer reference Library->>Loader: Queue/promote Loader->>Worker: Immutable source snapshot - Worker-->>Loader: Private mesh data or cached mesh - Loader->>Worker: Convert source mesh data + Worker->>Worker: Parse/generate and convert, or read cached mesh Worker-->>Loader: Private mesh Loader->>Loader: Set section colors, update flags, and sort sections Loader->>Loader: Resolve textures and queue decode if needed @@ -316,7 +315,7 @@ indices. Fresh pieces, synths, and cached meshes all use `FinalizeSections()` on the UI thread to resolve color indices, refresh solid/translucent/default flags, and sort sections using the current palette. Workers build geometry without reading global colors or keeping palette snapshots. -A corrupt mesh cache is rebuilt privately. Failed builds are not saved +A corrupt mesh cache falls back to source parsing in the same worker job. Failed builds are not saved as successful mesh caches. Temporary disk textures are scoped by their resolved file identity, so same-name PNGs in different project directories remain distinct. @@ -540,7 +539,7 @@ The preferences dialog saves a changed color-config path before this setter runs so simultaneous stud/color changes load the new configuration once. Color-only changes call `LoadColors()` directly. -Stud-style changes pause dispatch, finish running phases, discard staging built +Stud-style changes pause dispatch, finish running jobs, 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. Published ordinary meshes are rebuilt selectively using `HasStyleStud`, including