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.
This commit is contained in:
Leonardo Zide committed 2026-10-15 14:37:07 -07:00
1 parent 9f7e4d3a41
commit 4129788f95
7 files changed
+107 -183

No files matched your search

+9 -81
View File
@@ -14,7 +14,7 @@ constexpr QEvent::Type lcAssetLoaderCompletionEvent = static_cast<QEvent::Type>(
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<PieceRequest>& 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<SynthRequest>& 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<PieceRequest>& RequestedPiece : mPieceQueue)
{
const bool SkipCache = RequestedPiece->Source.SkipCache;
RequestedPiece->Source = mLibrary->SnapshotPieceSource(RequestedPiece->Info);
RequestedPiece->Source.SkipCache = SkipCache;
}
mPaused = false;
StartWorkersLocked();
+10 -10
View File
@@ -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<QString, std::shared_ptr<const lcEmbeddedData>> 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<lcLibraryMeshData> MeshData;
std::unique_ptr<lcMesh> 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<lcLibraryMeshData> MeshData; // Present when the next worker phase is mesh conversion.
std::unique_ptr<lcMesh> 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<lcLibraryMeshData> MeshData; // Present when the next worker phase is mesh conversion.
std::unique_ptr<lcMesh> StagedMesh;
QString Error;
};
@@ -159,7 +154,6 @@ private:
std::shared_ptr<TextureRequest> RequestedTexture;
lcTextureBuildResult TextureResult;
std::shared_ptr<SynthRequest> RequestedSynth;
std::unique_ptr<lcLibraryMeshData> SynthMeshData;
std::unique_ptr<lcMesh> 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<const lcPiece*, QString> mSynthErrors;
std::unordered_set<lcPiece*> mSynthPieces;
// Pending worker phases remain here while paused; mPaused prevents dispatch.
std::deque<std::shared_ptr<PieceRequest>> mPieceQueue;
std::deque<std::shared_ptr<TextureRequest>> mTextureQueue;
std::deque<std::shared_ptr<SynthRequest>> mSynthQueue;
std::deque<Completion> mCompletions;
// UI-thread only; public signals are deferred until outside synchronous waits.
std::deque<PieceNotification> mPieceNotifications;
std::deque<SynthNotification> mSynthNotifications;
std::vector<QFuture<void>> mFutures;
int mActiveWorkers = 0;
bool mPaused = false;
+68 -70
View File
@@ -1823,10 +1823,34 @@ lcPieceBuildResult lcPiecesLibrary::BuildPieceData(const lcPieceSourceSnapshot&
return Result;
}
if (!Source.InlineModel && Source.ZipFileType != lcZipFileType::Count && mZipFiles[static_cast<int>(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<int>(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<int>(Source.ZipFileType)])
{
lcMemFile PieceFile;
if (mZipFiles[static_cast<int>(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<int>(Source.ZipFileType)])
{
Result.MeshData->Clear();
lcMemFile PieceFile;
PieceFile.SetFileName(QDir(Source.LibraryDirectory).absoluteFilePath(QStringLiteral("unofficial/parts/") + Source.FileName));
if (mZipFiles[static_cast<int>(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;
}
+1 -1
View File
@@ -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);
+7 -9
View File
@@ -4,6 +4,7 @@
#include "lc_application.h"
#include "lc_file.h"
#include "lc_meshloader.h"
#include "lc_mesh.h"
#include "pieceinf.h"
#include <locale.h>
@@ -1591,7 +1592,7 @@ void lcSynthInfoUniversalJoint::AddParts(lcMemFile& File, lcLibraryMeshData&, co
File.WriteBuffer(Line, strlen(Line));
}
std::unique_ptr<lcLibraryMeshData> lcSynthInfo::BuildMeshData(const std::vector<lcPieceControlPoint>& ControlPoints, const std::atomic_bool& Cancelled) const
std::unique_ptr<lcMesh> lcSynthInfo::BuildMesh(const std::vector<lcPieceControlPoint>& ControlPoints, const std::atomic_bool& Cancelled) const
{
if (Cancelled.load(std::memory_order_relaxed))
return nullptr;
@@ -1603,10 +1604,10 @@ std::unique_ptr<lcLibraryMeshData> lcSynthInfo::BuildMeshData(const std::vector<
if (Cancelled.load(std::memory_order_relaxed))
return nullptr;
std::unique_ptr<lcLibraryMeshData> 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<lcLibraryMeshData> 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<lcMesh>(MeshData.CreateMesh(&Cancelled));
return nullptr;
}
+2 -1
View File
@@ -4,6 +4,7 @@
#include "piece.h"
class lcLibraryMeshData;
class lcMesh;
class lcSynthInfo
{
@@ -39,7 +40,7 @@ public:
virtual void GetDefaultControlPoints(std::vector<lcPieceControlPoint>& ControlPoints) const = 0;
virtual void VerifyControlPoints(std::vector<lcPieceControlPoint>& ControlPoints) const = 0;
int InsertControlPoint(std::vector<lcPieceControlPoint>& ControlPoints, const lcVector3& Start, const lcVector3& End) const;
std::unique_ptr<lcLibraryMeshData> BuildMeshData(const std::vector<lcPieceControlPoint>& ControlPoints, const std::atomic_bool& Cancelled) const;
std::unique_ptr<lcMesh> BuildMesh(const std::vector<lcPieceControlPoint>& ControlPoints, const std::atomic_bool& Cancelled) const;
protected:
using SectionCallbackFunc = std::function<void(const lcVector3& CurvePoint, quint32 SegmentIndex, float t)>;
+10 -11
View File
@@ -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