Unify mesh color resolution and section finalization

Cached meshes retained color-dependent flags and section ordering from the palette used when they were saved. Finalize fresh piece, synth, and cached mesh sections on the UI thread so color indices, flags, and ordering all reflect the current palette before publication.

Keep color codes in private mesh sections until finalization and remove separate color-code vectors, worker palette snapshots, and the early color-resolution path. Geometry conversion now runs without global color access or texture lookup callbacks.
This commit is contained in:
Leonardo Zide committed 2026-10-15 14:11:05 -07:00
1 parent f4fe4f8e87
commit 9f7e4d3a41
8 files changed
+84 -150

No files matched your search

+6 -34
View File
@@ -267,7 +267,7 @@ void lcAssetLoader::RunWorker()
if (RequestedPiece->MeshData)
{
Completed.PieceResult.Mesh.reset(RequestedPiece->MeshData->CreateMeshResolved(RequestedPiece->ColorTranslucency, RequestedPiece->DefaultColorIndex, nullptr));
Completed.PieceResult.Mesh.reset(RequestedPiece->MeshData->CreateMesh(nullptr));
RequestedPiece->MeshData.reset();
if (!Completed.PieceResult.Mesh)
@@ -289,7 +289,7 @@ void lcAssetLoader::RunWorker()
RequestedSynth->MeshData.reset();
else if (RequestedSynth->MeshData)
{
Completed.SynthMesh.reset(RequestedSynth->MeshData->CreateMeshResolved(RequestedSynth->ColorTranslucency, RequestedSynth->DefaultColorIndex, &RequestedSynth->Cancelled));
Completed.SynthMesh.reset(RequestedSynth->MeshData->CreateMesh(&RequestedSynth->Cancelled));
RequestedSynth->MeshData.reset();
}
else
@@ -476,8 +476,7 @@ void lcAssetLoader::ProcessPieceCompletion(Completion& Completed)
}
else if (Completed.PieceResult.MeshData)
{
// Register color indices on the UI thread, then queue the expensive mesh conversion.
ResolveMeshColors(*Completed.PieceResult.MeshData, RequestedPiece->ColorTranslucency, RequestedPiece->DefaultColorIndex);
// Queue the expensive geometry conversion; colors are finalized after it completes.
RequestedPiece->MeshData = std::move(Completed.PieceResult.MeshData);
Info->mState = lcPieceInfoState::Queued;
@@ -490,20 +489,9 @@ void lcAssetLoader::ProcessPieceCompletion(Completion& Completed)
}
else if (Completed.PieceResult.Mesh)
{
RequestedPiece->ColorTranslucency.clear();
RequestedPiece->StagedMesh = std::move(Completed.PieceResult.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.PieceResult.FromCache)
Section.ColorIndex = lcGetColorIndex(Completed.PieceResult.ColorCodes[ColorIndex++]);
}
}
RequestedPiece->StagedMesh->FinalizeSections();
RequestedPiece->SaveCache = !Completed.PieceResult.FromCache;
}
@@ -549,8 +537,7 @@ void lcAssetLoader::ProcessSynthCompletion(Completion& Completed)
if (Completed.SynthMeshData)
{
// Resolve global colors here, then send expensive conversion back to a worker.
ResolveMeshColors(*Completed.SynthMeshData, RequestedSynth->ColorTranslucency, RequestedSynth->DefaultColorIndex);
// Queue the expensive geometry conversion; colors are finalized after it completes.
RequestedSynth->MeshData = std::move(Completed.SynthMeshData);
QMutexLocker QueueLock(&mQueueMutex);
@@ -568,8 +555,8 @@ void lcAssetLoader::ProcessSynthCompletion(Completion& Completed)
return;
}
RequestedSynth->ColorTranslucency.clear();
RequestedSynth->StagedMesh = std::move(Completed.SynthMesh);
RequestedSynth->StagedMesh->FinalizeSections();
const bool Ready = ResolveAndQueueMeshTextures(*RequestedSynth->StagedMesh, std::vector<QString>(),
std::map<QString, std::shared_ptr<const lcEmbeddedData>>(), RequestedSynth->LoadPriority, RequestedSynth->Error);
@@ -611,19 +598,6 @@ void lcAssetLoader::ProcessTextureCompletion(Completion& Completed)
}
}
void lcAssetLoader::ResolveMeshColors(lcLibraryMeshData& MeshData, std::vector<bool>& 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<QString>& SearchDirectories,
const std::map<QString, std::shared_ptr<const lcEmbeddedData>>& EmbeddedData, lcPieceLoadPriority LoadPriority, QString& Error)
{
@@ -1457,7 +1431,6 @@ void lcAssetLoader::PauseQueuedWork()
if (RequestedPiece->MeshData)
{
RequestedPiece->MeshData.reset();
RequestedPiece->ColorTranslucency.clear();
RequestedPiece->Info->mState = lcPieceInfoState::Queued;
}
@@ -1480,7 +1453,6 @@ void lcAssetLoader::PauseQueuedWork()
RequestedSynth->MeshData.reset();
RequestedSynth->StagedMesh.reset();
RequestedSynth->ColorTranslucency.clear();
if (std::find(mSynthQueue.begin(), mSynthQueue.end(), RequestedSynth) == mSynthQueue.end())
mSynthQueue.push_back(RequestedSynth);
-6
View File
@@ -42,7 +42,6 @@ struct lcPieceBuildResult
std::unique_ptr<lcLibraryMeshData> MeshData;
std::unique_ptr<lcMesh> Mesh;
std::vector<quint32> ColorCodes;
bool FromCache = false;
bool EmptyGeometry = false;
QString Error;
@@ -113,8 +112,6 @@ private:
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::vector<bool> ColorTranslucency;
int DefaultColorIndex = 0;
std::unique_ptr<lcMesh> StagedMesh;
bool EmptyGeometry = false;
bool SaveCache = false;
@@ -146,8 +143,6 @@ private:
std::atomic_bool Cancelled = false;
bool Terminal = false;
std::unique_ptr<lcLibraryMeshData> MeshData; // Present when the next worker phase is mesh conversion.
std::vector<bool> ColorTranslucency;
int DefaultColorIndex = 0;
std::unique_ptr<lcMesh> StagedMesh;
QString Error;
};
@@ -197,7 +192,6 @@ private:
void ProcessPieceCompletion(Completion& Completed);
void ProcessSynthCompletion(Completion& Completed);
void ProcessTextureCompletion(Completion& Completed);
static void ResolveMeshColors(lcLibraryMeshData& MeshData, std::vector<bool>& ColorTranslucency, int& DefaultColorIndex);
bool ResolveAndQueueMeshTextures(lcMesh& Mesh, const std::vector<QString>& SearchDirectories,
const std::map<QString, std::shared_ptr<const lcEmbeddedData>>& EmbeddedData, lcPieceLoadPriority LoadPriority, QString& Error);
static bool MeshTexturesSettled(const lcMesh& Mesh, QString& Error);
+1 -1
View File
@@ -1874,7 +1874,7 @@ lcPieceBuildResult lcPiecesLibrary::BuildPieceData(const lcPieceSourceSnapshot&
Result.FromCache = true;
Result.Mesh.reset(new lcMesh);
if (!Result.Mesh->FileLoad(CacheData, Result.ColorCodes))
if (!Result.Mesh->FileLoad(CacheData))
Result.Mesh.reset();
return Result;
+52 -5
View File
@@ -432,10 +432,8 @@ void lcMesh::ExportWavefrontIndices(lcFile& File, int DefaultColorIndex, int Ver
ExportWavefrontIndices<GLuint>(File, DefaultColorIndex, VertexOffset);
}
bool lcMesh::FileLoad(lcMemFile& File, std::vector<quint32>& ColorCodes)
bool lcMesh::FileLoad(lcMemFile& File)
{
ColorCodes.clear();
quint32 FileId, FileVersion, Flags;
if (File.ReadU32(&FileId, 1) != 1 || FileId != LC_MESH_FILE_ID || File.ReadU32(&FileVersion, 1) != 1 ||
@@ -487,8 +485,7 @@ bool lcMesh::FileLoad(lcMemFile& File, std::vector<quint32>& ColorCodes)
File.ReadU32(&DrawCount, 1) != 1 || File.ReadU16(&PrimtiveType, 1) != 1)
return false;
Section.ColorIndex = 0;
ColorCodes.push_back(ColorCode);
Section.ColorIndex = static_cast<int>(ColorCode);
Section.DrawOffset = DrawOffset;
Section.DrawCount = DrawCount;
Section.PrimitiveType = (lcMeshPrimitiveType)PrimtiveType;
@@ -538,6 +535,56 @@ bool lcMesh::FileLoad(lcMemFile& File, std::vector<quint32>& ColorCodes)
return true;
}
void lcMesh::FinalizeSections()
{
mFlags &= ~(lcMeshFlag::HasDefault | lcMeshFlag::HasSolid | lcMeshFlag::HasTranslucent);
for (lcMeshLod& Lod : mLods)
{
for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++)
{
lcMeshSection& Section = Lod.Sections[SectionIdx];
Section.ColorIndex = lcGetColorIndex(static_cast<quint32>(Section.ColorIndex));
if (Section.PrimitiveType != LC_MESH_TRIANGLES && Section.PrimitiveType != LC_MESH_TEXTURED_TRIANGLES)
continue;
if (Section.ColorIndex == gDefaultColor)
mFlags |= lcMeshFlag::HasDefault;
else if (lcIsColorTranslucent(Section.ColorIndex))
mFlags |= lcMeshFlag::HasTranslucent;
else
mFlags |= lcMeshFlag::HasSolid;
}
if (Lod.NumSections > 1)
std::sort(Lod.Sections, Lod.Sections + Lod.NumSections, [](const lcMeshSection& Left, const lcMeshSection& Right)
{
if (Left.PrimitiveType != Right.PrimitiveType)
{
const lcMeshPrimitiveType PrimitiveOrder[] =
{
LC_MESH_TRIANGLES,
LC_MESH_TEXTURED_TRIANGLES,
LC_MESH_LINES,
LC_MESH_CONDITIONAL_LINES
};
return std::find(std::begin(PrimitiveOrder), std::end(PrimitiveOrder), Left.PrimitiveType) <
std::find(std::begin(PrimitiveOrder), std::end(PrimitiveOrder), Right.PrimitiveType);
}
const bool LeftTranslucent = lcIsColorTranslucent(Left.ColorIndex);
const bool RightTranslucent = lcIsColorTranslucent(Right.ColorIndex);
if (LeftTranslucent != RightTranslucent)
return !LeftTranslucent;
return Left.ColorIndex > Right.ColorIndex;
});
}
}
bool lcMesh::FileSave(lcMemFile& File)
{
File.WriteU32(LC_MESH_FILE_ID);
+6 -2
View File
@@ -34,7 +34,7 @@ struct lcVertexConditional
struct lcMeshSection
{
int ColorIndex;
int ColorIndex; // Holds a color code until FinalizeSections() converts it to an index.
int DrawOffset; // Index-buffer byte offset, or first conditional vertex.
int DrawCount; // Index count, or conditional vertex count.
lcMeshPrimitiveType PrimitiveType;
@@ -84,9 +84,13 @@ public:
void Create(quint16 (&NumSections)[LC_NUM_MESH_LODS], int VertexCount, int TexturedVertexCount, int ConditionalVertexCount, int IndexCount);
void CreateBox();
bool FileLoad(lcMemFile& File, std::vector<quint32>& ColorCodes);
bool FileLoad(lcMemFile& File);
bool FileSave(lcMemFile& File);
// Set colors and flags, then sort sections using the current palette on the UI thread.
// Converts stored color codes to indices once, before publishing the mesh.
void FinalizeSections();
template<typename IndexType>
void ExportPOVRay(lcFile& File, const char* MeshName, const char** ColorTable);
void ExportPOVRay(lcFile& File, const char* MeshName, const char** ColorTable);
+7 -92
View File
@@ -606,39 +606,6 @@ lcMeshLoaderMaterial* lcLibraryMeshData::GetTexturedMaterial(quint32 ColorCode,
return Material;
}
static bool lcMeshLoaderFinalSectionCompare(const lcMeshLoaderFinalSection& a, const lcMeshLoaderFinalSection& b, const std::vector<bool>& ColorTranslucency)
{
if (a.PrimitiveType != b.PrimitiveType)
{
const lcMeshPrimitiveType PrimitiveOrder[LC_MESH_NUM_PRIMITIVE_TYPES] =
{
LC_MESH_TRIANGLES,
LC_MESH_TEXTURED_TRIANGLES,
LC_MESH_LINES,
LC_MESH_CONDITIONAL_LINES
};
for (int PrimitiveType = 0; PrimitiveType < LC_MESH_NUM_PRIMITIVE_TYPES; PrimitiveType++)
{
const lcMeshPrimitiveType Primitive = PrimitiveOrder[PrimitiveType];
if (a.PrimitiveType == Primitive)
return true;
if (b.PrimitiveType == Primitive)
return false;
}
}
const bool TranslucentA = ColorTranslucency[a.Color];
const bool TranslucentB = ColorTranslucency[b.Color];
if (TranslucentA != TranslucentB)
return !TranslucentA;
return a.Color > b.Color;
}
quint32 lcLibraryMeshData::AddTexturedVertex(const lcVector3& Position, const lcVector3& Normal, const lcVector2& TexCoords)
{
for (int VertexIndex = static_cast<int>(mTexturedVertices.size()) - 1; VertexIndex >= 0; VertexIndex--)
@@ -803,33 +770,7 @@ void lcLibraryMeshData::GenerateTexturedVertices()
}
}
lcMesh* lcLibraryMeshData::CreateMesh(const std::function<lcTexture*(const char*)>& TextureLookup)
{
ResolveColors();
std::vector<bool> ColorTranslucency;
ColorTranslucency.reserve(gColorList.size());
for (const lcColor& Color : gColorList)
ColorTranslucency.push_back(Color.Translucent);
return CreateMeshInternal(TextureLookup, ColorTranslucency, gDefaultColor, nullptr);
}
void lcLibraryMeshData::ResolveColors()
{
for (const std::unique_ptr<lcMeshLoaderMaterial>& Material : mMaterials)
Material->Color = lcGetColorIndex(Material->Color);
}
lcMesh* lcLibraryMeshData::CreateMeshResolved(const std::vector<bool>& ColorTranslucency, int DefaultColorIndex, const std::atomic_bool* Cancelled)
{
// Keep project texture lookup on the UI thread after conversion.
const std::function<lcTexture*(const char*)> DeferTexture = [](const char*) -> lcTexture* { return nullptr; };
return CreateMeshInternal(DeferTexture, ColorTranslucency, DefaultColorIndex, Cancelled);
}
lcMesh* lcLibraryMeshData::CreateMeshInternal(const std::function<lcTexture*(const char*)>& TextureLookup, const std::vector<bool>& ColorTranslucency, int DefaultColorIndex, const std::atomic_bool* Cancelled)
lcMesh* lcLibraryMeshData::CreateMesh(const std::atomic_bool* Cancelled)
{
if (Cancelled && Cancelled->load(std::memory_order_relaxed))
return nullptr;
@@ -890,10 +831,6 @@ lcMesh* lcLibraryMeshData::CreateMeshInternal(const std::function<lcTexture*(con
}
NumSections[LodIdx] = static_cast<quint16>(FinalSections[LodIdx].size());
std::sort(FinalSections[LodIdx].begin(), FinalSections[LodIdx].end(), [&ColorTranslucency](const lcMeshLoaderFinalSection& Left, const lcMeshLoaderFinalSection& Right)
{
return lcMeshLoaderFinalSectionCompare(Left, Right, ColorTranslucency);
});
}
if (Cancelled && Cancelled->load(std::memory_order_relaxed))
@@ -962,9 +899,9 @@ lcMesh* lcLibraryMeshData::CreateMeshInternal(const std::function<lcTexture*(con
bool SectionsWritten;
if (Mesh->mIndexType == GL_UNSIGNED_SHORT)
SectionsWritten = WriteSections<quint16>(Mesh, FinalSections, BaseVertices, TextureLookup, ColorTranslucency, DefaultColorIndex, Cancelled);
SectionsWritten = WriteSections<quint16>(Mesh, FinalSections, BaseVertices, Cancelled);
else
SectionsWritten = WriteSections<quint32>(Mesh, FinalSections, BaseVertices, TextureLookup, ColorTranslucency, DefaultColorIndex, Cancelled);
SectionsWritten = WriteSections<quint32>(Mesh, FinalSections, BaseVertices, Cancelled);
if (!SectionsWritten)
{
@@ -1063,7 +1000,7 @@ lcMesh* lcLibraryMeshData::CreateMeshInternal(const std::function<lcTexture*(con
}
template<typename IndexType>
bool lcLibraryMeshData::WriteSections(lcMesh* Mesh, const std::vector<lcMeshLoaderFinalSection> (&FinalSections)[LC_NUM_MESH_LODS], int(&BaseVertices)[LC_NUM_MESHDATA_TYPES], const std::function<lcTexture*(const char*)>& TextureLookup, const std::vector<bool>& ColorTranslucency, int DefaultColorIndex, const std::atomic_bool* Cancelled)
bool lcLibraryMeshData::WriteSections(lcMesh* Mesh, const std::vector<lcMeshLoaderFinalSection> (&FinalSections)[LC_NUM_MESH_LODS], int(&BaseVertices)[LC_NUM_MESHDATA_TYPES], const std::atomic_bool* Cancelled)
{
int NumIndices = 0;
@@ -1077,22 +1014,12 @@ bool lcLibraryMeshData::WriteSections(lcMesh* Mesh, const std::vector<lcMeshLoad
const lcMeshLoaderFinalSection& FinalSection = FinalSections[LodIdx][SectionIndex];
lcMeshSection& DstSection = Mesh->mLods[LodIdx].Sections[SectionIndex];
DstSection.ColorIndex = FinalSection.Color;
DstSection.ColorIndex = static_cast<int>(FinalSection.Color);
DstSection.PrimitiveType = FinalSection.PrimitiveType;
DstSection.DrawCount = 0;
DstSection.TextureName = QString::fromLatin1(FinalSection.Name);
if (!FinalSection.Name[0])
DstSection.Texture = nullptr;
else
{
if (TextureLookup)
DstSection.Texture = TextureLookup(FinalSection.Name);
else if (mMeshLoader)
DstSection.Texture = lcGetPiecesLibrary()->FindTexture(FinalSection.Name, mMeshLoader->mCurrentProject, mMeshLoader->SearchProjectFolder());
else
DstSection.Texture = lcGetPiecesLibrary()->FindTexture(FinalSection.Name, nullptr, false);
}
DstSection.Texture = nullptr;
DstSection.DrawOffset = NumIndices * sizeof(IndexType);
@@ -1156,19 +1083,7 @@ bool lcLibraryMeshData::WriteSections(lcMesh* Mesh, const std::vector<lcMeshLoad
if (!AddSection(Section.get(), MeshDataType))
return false;
if (DstSection.PrimitiveType == LC_MESH_TRIANGLES || DstSection.PrimitiveType == LC_MESH_TEXTURED_TRIANGLES)
{
if (DstSection.ColorIndex == DefaultColorIndex)
Mesh->mFlags |= lcMeshFlag::HasDefault;
else
{
if (ColorTranslucency[DstSection.ColorIndex])
Mesh->mFlags |= lcMeshFlag::HasTranslucent;
else
Mesh->mFlags |= lcMeshFlag::HasSolid;
}
}
else
if (DstSection.PrimitiveType != LC_MESH_TRIANGLES && DstSection.PrimitiveType != LC_MESH_TEXTURED_TRIANGLES)
Mesh->mFlags |= lcMeshFlag::HasLines;
if (DstSection.PrimitiveType == LC_MESH_TEXTURED_TRIANGLES)
+3 -5
View File
@@ -165,9 +165,8 @@ public:
mMeshLoader = MeshLoader;
}
lcMesh* CreateMesh(const std::function<lcTexture*(const char*)>& TextureLookup = {});
void ResolveColors();
lcMesh* CreateMeshResolved(const std::vector<bool>& ColorTranslucency, int DefaultColorIndex, const std::atomic_bool* Cancelled);
// Build private geometry with color codes; the UI thread calls FinalizeSections().
lcMesh* CreateMesh(const std::atomic_bool* Cancelled);
void AddVertices(lcMeshDataType MeshDataType, size_t VertexCount, int* BaseVertex, lcMeshLoaderVertex** VertexBuffer);
void AddIndices(lcMeshDataType MeshDataType, lcMeshPrimitiveType PrimitiveType, quint32 ColorCode, size_t IndexCount, quint32** IndexBuffer);
void AddMeshData(const lcLibraryMeshData& Data, const lcMatrix44& Transform, quint32 CurrentColorCode, bool InvertWinding, bool InvertNormals, lcMeshLoaderTextureMap* TextureMap, lcMeshDataType OverrideDestIndex);
@@ -192,8 +191,7 @@ protected:
quint32 AddTexturedVertex(const lcVector3& Position, const lcVector3& Normal, const lcVector2& TexCoords);
template<typename IndexType>
bool WriteSections(lcMesh* Mesh, const std::vector<lcMeshLoaderFinalSection> (&FinalSections)[LC_NUM_MESH_LODS], int (&BaseVertices)[LC_NUM_MESHDATA_TYPES], const std::function<lcTexture*(const char*)>& TextureLookup, const std::vector<bool>& ColorTranslucency, int DefaultColorIndex, const std::atomic_bool* Cancelled);
lcMesh* CreateMeshInternal(const std::function<lcTexture*(const char*)>& TextureLookup, const std::vector<bool>& ColorTranslucency, int DefaultColorIndex, const std::atomic_bool* Cancelled);
bool WriteSections(lcMesh* Mesh, const std::vector<lcMeshLoaderFinalSection> (&FinalSections)[LC_NUM_MESH_LODS], int (&BaseVertices)[LC_NUM_MESHDATA_TYPES], const std::atomic_bool* Cancelled);
static bool UpdateMeshBoundingBox(lcMesh* Mesh, const std::atomic_bool* Cancelled);
template<typename IndexType>
+9 -5
View File
@@ -283,7 +283,7 @@ worker scheduling and `RunWorker()`, UI-thread `ProcessCompletions()` dispatch t
`ProcessPieceCompletion()`, `ProcessSynthCompletion()`, or `ProcessTextureCompletion()`, 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,
Piece and synth handlers share mesh color finalization and texture dependency checks,
while keeping their distinct ownership and publication rules explicit.
```mermaid
@@ -297,9 +297,9 @@ sequenceDiagram
Library->>Loader: Queue/promote
Loader->>Worker: Immutable source snapshot
Worker-->>Loader: Private mesh data or cached mesh
Loader->>Loader: Resolve global colors
Loader->>Worker: Convert source mesh data
Worker-->>Loader: Private mesh
Loader->>Loader: Set section colors, update flags, and sort sections
Loader->>Loader: Resolve textures and queue decode if needed
Loader->>Worker: Texture source snapshot
Worker-->>Loader: Decoded CPU image
@@ -310,9 +310,13 @@ sequenceDiagram
Texture-free meshes skip decoding/upload; cached meshes skip source parsing and
conversion. Both paths use the same dependency and publication rules. Cache
reads and mesh deserialization run in the library's worker build service; cache
bytes stay local, and the result records whether it came from cache. Source mesh
color indices are resolved on the UI thread before conversion uses a color
snapshot. A corrupt mesh cache is rebuilt privately. Failed builds are not saved
bytes stay local, and the result records whether it came from cache. Private mesh
sections store color codes in `ColorIndex` until finalization converts them to
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
as successful mesh caches. Temporary disk textures are scoped by their resolved
file identity, so same-name PNGs in different project directories remain distinct.