Align texture acquisition and scoped ownership

Texture lookup previously acquired references implicitly, and mesh sections and
loader requests paired raw texture pointers with manual release paths. Separate
borrowed lookup from scoped acquisition and explicit load requests/waits to match
the piece data ownership API.

Store lcTextureRef in mesh sections and loader requests, retain references on
section copies, and reset worker-shared request handles on the UI thread before
completion, cancellation, or shutdown. Keep reference primitives internal and use
scoped acquisitions for merge validation. Remove redundant section initialization
and synth-notification resets, and document the resulting ownership contracts.

Validation: macOS Debug build, diff whitespace checks, and 30 offscreen lifecycle
checks covering reference copies/moves, texture dependencies, cancellation,
shutdown, merge conflicts, and existing piece ownership paths. Successful GPU
upload/rendering is not covered by the offscreen checks.
This commit is contained in:
Leonardo Zide committed 2026-10-19 10:38:51 -07:00
1 parent 61be531444
commit 98c67f3705
12 files changed
+103 -86

No files matched your search

+14 -23
View File
@@ -68,7 +68,7 @@ void lcAssetLoader::PromotePiecePriority(const std::shared_ptr<PieceRequest>& Re
{
for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++)
{
const auto It = mTextureRequests.find(Lod.Sections[SectionIdx].Texture);
const auto It = mTextureRequests.find(Lod.Sections[SectionIdx].Texture.Get());
if (It != mTextureRequests.end())
It->second->LoadPriority = std::max(It->second->LoadPriority, RequestedPiece->LoadPriority);
@@ -119,12 +119,11 @@ void lcAssetLoader::RequestTextureLoad(lcTexture* Texture, lcFileSource Source,
return;
}
mLibrary->AddTextureReference(Texture);
Texture->mState = lcAssetState::Loading;
std::shared_ptr<TextureRequest> RequestedTexture = std::make_shared<TextureRequest>();
RequestedTexture->Texture = Texture;
RequestedTexture->Texture = lcTextureRef(Texture);
RequestedTexture->Source = std::move(Source);
RequestedTexture->LoadPriority = LoadPriority;
RequestedTexture->QueuedAt = QDateTime::currentMSecsSinceEpoch();
@@ -412,7 +411,7 @@ void lcAssetLoader::ProcessTextureCompletion(Completion& Completed)
Q_ASSERT(QThread::currentThread() == thread());
const std::shared_ptr<TextureRequest>& RequestedTexture = Completed.RequestedTexture;
lcTexture* Texture = RequestedTexture->Texture;
lcTexture* Texture = RequestedTexture->Texture.Get();
const auto It = mTextureRequests.find(Texture);
const bool Stale = mStopping || It == mTextureRequests.end() || It->second != RequestedTexture;
if (Stale && It != mTextureRequests.end() && It->second == RequestedTexture)
@@ -421,7 +420,7 @@ void lcAssetLoader::ProcessTextureCompletion(Completion& Completed)
if (Stale)
{
mLibrary->ReleaseTexture(Texture);
RequestedTexture->Texture.Reset();
return;
}
@@ -446,10 +445,10 @@ bool lcAssetLoader::ResolveAndQueueMeshTextures(lcMesh& Mesh, const lcPieceSourc
continue;
lcFileSource TextureSource;
Section.Texture = mLibrary->FindTextureDeferred(Section.TextureName.toLatin1().constData(), Source.TextureSearchDirectories, Source.EmbeddedData, TextureSource);
Section.Texture = mLibrary->AcquireTexture(Section.TextureName.toLatin1().constData(), Source.TextureSearchDirectories, Source.EmbeddedData, TextureSource);
if (Section.Texture && !Section.Texture->IsReady() && Section.Texture->mState != lcAssetState::Failed)
RequestTextureLoad(Section.Texture, std::move(TextureSource), LoadPriority);
RequestTextureLoad(Section.Texture.Get(), std::move(TextureSource), LoadPriority);
}
return MeshTexturesSettled(Mesh, Error);
@@ -463,7 +462,7 @@ bool lcAssetLoader::MeshTexturesSettled(const lcMesh& Mesh, QString& Error)
for (int SectionIdx = 0; SectionIdx < Lod.NumSections; SectionIdx++)
{
const lcMeshSection& Section = Lod.Sections[SectionIdx];
lcTexture* Texture = Section.Texture;
lcTexture* Texture = Section.Texture.Get();
if (!Texture)
{
@@ -483,7 +482,7 @@ void lcAssetLoader::UploadTextureRequest(const std::shared_ptr<TextureRequest>&
{
Q_ASSERT(QThread::currentThread() == thread());
lcTexture* Texture = RequestedTexture->Texture;
lcTexture* Texture = RequestedTexture->Texture.Get();
RequestedTexture->UploadAttempts++;
@@ -548,12 +547,12 @@ void lcAssetLoader::CompleteTextureRequest(const std::shared_ptr<TextureRequest>
{
Q_ASSERT(QThread::currentThread() == thread());
const auto It = mTextureRequests.find(RequestedTexture->Texture);
const auto It = mTextureRequests.find(RequestedTexture->Texture.Get());
if (It != mTextureRequests.end() && It->second == RequestedTexture)
mTextureRequests.erase(It);
RequestedTexture->State = RequestedTexture->Texture->IsReady() ? RequestState::Succeeded : RequestState::Failed;
mLibrary->ReleaseTexture(RequestedTexture->Texture);
RequestedTexture->Texture.Reset();
FinishPiecesWithSettledTextures();
FinishSynthsWithSettledTextures();
@@ -718,8 +717,6 @@ void lcAssetLoader::DispatchNotifications()
return;
}
}
Notification.PieceData.Reset();
}
if (!mPieceNotifications.empty() || !mSynthNotifications.empty())
@@ -851,7 +848,7 @@ bool lcAssetLoader::WaitForSynthMeshes(const std::vector<lcPiece*>& Pieces)
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);
const TextureRequestMap::iterator TextureIt = mTextureRequests.find(Lod.Sections[SectionIdx].Texture.Get());
if (TextureIt != mTextureRequests.end())
TextureIt->second->LoadPriority = lcLoadPriority::Blocking;
@@ -1041,11 +1038,11 @@ void lcAssetLoader::CancelUnusedTextureRequests()
for (const std::shared_ptr<TextureRequest>& RequestedTexture : Cancelled)
{
lcTexture* Texture = RequestedTexture->Texture;
lcTexture* Texture = RequestedTexture->Texture.Get();
Texture->DiscardDecodedImage();
mLibrary->ReleaseTexture(Texture);
RequestedTexture->Texture.Reset();
}
}
@@ -1163,7 +1160,7 @@ void lcAssetLoader::Shutdown()
RequestedPiece->PieceData.Reset();
for (const std::shared_ptr<TextureRequest>& RequestedTexture : CancelledTextures)
mLibrary->ReleaseTexture(RequestedTexture->Texture);
RequestedTexture->Texture.Reset();
WaitForAllWork();
@@ -1177,13 +1174,7 @@ void lcAssetLoader::Shutdown()
}
while (!mSynthNotifications.empty())
{
SynthNotification Notification = std::move(mSynthNotifications.front());
mSynthNotifications.pop_front();
Notification.PieceData.Reset();
}
mLibrary->mCancelLoading.store(WasCancelled);
}
+7 -4
View File
@@ -51,6 +51,9 @@ public:
// Start or promote decoding as needed, then wait through upload and retries. The caller retains the texture.
bool WaitForTexture(lcTexture* Texture, lcFileSource Source);
// Queue or promote decoding without waiting or acquiring a consumer reference.
void RequestTextureLoad(lcTexture* Texture, lcFileSource Source, lcLoadPriority LoadPriority);
// True while piece, texture, or generated-mesh requests or worker completions remain unsettled.
// Includes paused work and upload retries; deferred notifications alone do not count.
bool HasPendingWork();
@@ -100,10 +103,11 @@ private:
QString Error;
};
// Holds a texture reference through CPU decoding and UI-thread upload/retries.
// Owns a texture reference through CPU decoding and UI-thread upload/retries.
// Reset on the UI thread before worker-shared request storage can be destroyed.
struct TextureRequest : RequestBase
{
lcTexture* Texture;
lcTextureRef Texture;
lcFileSource Source;
int UploadAttempts = 0;
qint64 RetryAt = 0;
@@ -151,8 +155,7 @@ private:
lcPieceDataRef PieceData;
};
// Queue and promote requests on the UI thread.
void RequestTextureLoad(lcTexture* Texture, lcFileSource Source, lcLoadPriority LoadPriority);
// Promote requests on the UI thread.
void PromotePiecePriority(const std::shared_ptr<PieceRequest>& RequestedPiece, lcLoadPriority LoadPriority);
// Fill available job slots using queued request priority and age.
+23 -14
View File
@@ -507,7 +507,7 @@ bool lcPiecesLibrary::RemapProjectPieceData(lcPieceData* PieceData, const QStrin
return true;
}
lcTexture* lcPiecesLibrary::FindTexture(const char* TextureName, Project* CurrentProject, bool SearchProjectFolder)
lcTextureRef lcPiecesLibrary::AcquireTexture(const char* TextureName, Project* CurrentProject, bool SearchProjectFolder)
{
QString ProjectPath;
@@ -525,17 +525,7 @@ lcTexture* lcPiecesLibrary::FindTexture(const char* TextureName, Project* Curren
EmbeddedData = CurrentProject->GetEmbeddedDataLookup();
lcFileSource Source;
lcTexture* Texture = FindTextureDeferred(TextureName, std::vector<QString>{ ProjectPath }, EmbeddedData, Source);
if (!Texture)
return nullptr;
if (mAssetLoader->WaitForTexture(Texture, std::move(Source)))
return Texture;
ReleaseTexture(Texture);
return nullptr;
return AcquireTexture(TextureName, std::vector<QString>{ ProjectPath }, EmbeddedData, Source);
}
QString lcPiecesLibrary::FindProjectTextureFile(const QString& ProjectPath, const QString& TextureName)
@@ -683,12 +673,31 @@ lcFileSource lcPiecesLibrary::ResolveTextureSource(const char* TextureName, cons
return Source;
}
lcTexture* lcPiecesLibrary::FindTextureDeferred(const char* TextureName, const std::vector<QString>& SearchDirectories, const std::map<QString, std::shared_ptr<const lcEmbeddedData>>& EmbeddedData, lcFileSource& Source)
lcTexture* lcPiecesLibrary::FindTexture(const lcFileSource& Source)
{
return mTextureManager->FindTexture(Source);
}
lcTextureRef lcPiecesLibrary::AcquireTexture(const char* TextureName, const std::vector<QString>& SearchDirectories, const std::map<QString, std::shared_ptr<const lcEmbeddedData>>& EmbeddedData, lcFileSource& Source)
{
QString ProjectPath;
Source = ResolveTextureSource(TextureName, SearchDirectories, EmbeddedData, ProjectPath);
return mTextureManager->AcquireTexture(TextureName, Source, ProjectPath);
return lcTextureRef::Adopt(mTextureManager->AcquireTexture(TextureName, Source, ProjectPath), this);
}
void lcPiecesLibrary::RequestTextureLoad(lcTexture* Texture, lcLoadPriority LoadPriority)
{
Q_ASSERT(QThread::currentThread() == thread());
mAssetLoader->RequestTextureLoad(Texture, Texture->mSource, LoadPriority);
}
bool lcPiecesLibrary::WaitForTexture(const lcTextureRef& TextureRef)
{
Q_ASSERT(QThread::currentThread() == thread());
return mAssetLoader->WaitForTexture(TextureRef.Get(), TextureRef->mSource);
}
lcTextureBuildResult lcPiecesLibrary::BuildTextureData(const lcFileSource& Source)
+9 -6
View File
@@ -214,12 +214,14 @@ public:
lcPieceBuildResult BuildPieceData(const lcPieceSourceSnapshot& Source);
void SaveBuiltPieceDataCache(const lcPieceSourceSnapshot& Source, lcMesh& Mesh);
// UI-thread lookup and upload wait; returns a ready reference or nullptr on failure.
// Release a returned reference with ReleaseTexture().
lcTexture* FindTexture(const char* TextureName, Project* CurrentProject, bool SearchProjectFolder);
// UI-thread lookup/acquisition only; also returns the resolved source for loading. Caller releases the reference.
lcTexture* FindTextureDeferred(const char* TextureName, const std::vector<QString>& SearchDirectories, const std::map<QString, std::shared_ptr<const lcEmbeddedData>>& EmbeddedData, lcFileSource& Source);
void ReleaseTexture(lcTexture* Texture);
// Borrowed registered lookup by resolved source. No I/O, creation, or acquisition.
lcTexture* FindTexture(const lcFileSource& Source);
// Resolve/create and return one scoped reference without decoding, uploading, or waiting.
lcTextureRef AcquireTexture(const char* TextureName, Project* CurrentProject, bool SearchProjectFolder);
lcTextureRef AcquireTexture(const char* TextureName, const std::vector<QString>& SearchDirectories, const std::map<QString, std::shared_ptr<const lcEmbeddedData>>& EmbeddedData, lcFileSource& Source);
// Priority controls scheduling only. Both operations require the caller to retain the texture.
void RequestTextureLoad(lcTexture* Texture, lcLoadPriority LoadPriority);
bool WaitForTexture(const lcTextureRef& TextureRef);
bool PieceDataInCategory(lcPieceData* PieceData, const char* CategoryKeywords) const;
void GetCategoryEntries(int CategoryIndex, bool GroupPieces, std::vector<lcPieceData*>& SinglePieces, std::vector<lcPieceData*>& GroupedPieces);
@@ -330,6 +332,7 @@ protected:
// Worker decode service: returns private pixels/errors without GL upload or publication.
lcTextureBuildResult BuildTextureData(const lcFileSource& Source);
void AddTextureReference(lcTexture* Texture);
void ReleaseTexture(lcTexture* Texture);
static QString FindProjectTextureFile(const QString& ProjectPath, const QString& TextureName);
lcFileSource ResolveTextureSource(const char* TextureName, const std::vector<QString>& SearchDirectories, const std::map<QString, std::shared_ptr<const lcEmbeddedData>>& EmbeddedData, QString& ProjectPath);
-12
View File
@@ -20,13 +20,6 @@ lcMesh::~lcMesh()
free(mIndexData);
for (int LodIdx = 0; LodIdx < LC_NUM_MESH_LODS; LodIdx++)
{
for (int SectionIdx = 0; SectionIdx < mLods[LodIdx].NumSections; SectionIdx++)
{
lcTexture* Texture = mLods[LodIdx].Sections[SectionIdx].Texture;
if (Texture)
lcGetPiecesLibrary()->ReleaseTexture(Texture);
}
delete[] mLods[LodIdx].Sections;
}
}
@@ -134,7 +127,6 @@ void lcMesh::CreateBox()
Section->DrawOffset = 0;
Section->DrawCount = 36;
Section->PrimitiveType = LC_MESH_TRIANGLES;
Section->Texture = nullptr;
Section->BoundingBox = mBoundingBox;
Section->Radius = mRadius;
@@ -161,7 +153,6 @@ void lcMesh::CreateBox()
Section->DrawOffset = 36 * 2;
Section->DrawCount = 24;
Section->PrimitiveType = LC_MESH_LINES;
Section->Texture = nullptr;
Section->BoundingBox = mBoundingBox;
Section->Radius = mRadius;
@@ -511,10 +502,7 @@ bool lcMesh::FileLoad(lcMemFile& File)
FileName[Length] = 0;
Section.TextureName = QString::fromLatin1(FileName);
Section.Texture = nullptr;
}
else
Section.Texture = nullptr;
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "lc_math.h"
#include "lc_assettypes.h"
enum lcMeshPrimitiveType
{
@@ -38,7 +39,7 @@ struct lcMeshSection
int DrawOffset; // Index-buffer byte offset, or first conditional vertex.
int DrawCount; // Index count, or conditional vertex count.
lcMeshPrimitiveType PrimitiveType;
lcTexture* Texture = nullptr;
lcTextureRef Texture;
QString TextureName;
lcBoundingBox BoundingBox;
float Radius;
-2
View File
@@ -1019,8 +1019,6 @@ bool lcLibraryMeshData::WriteSections(lcMesh* Mesh, const std::vector<lcMeshLoad
DstSection.DrawCount = 0;
DstSection.TextureName = QString::fromLatin1(FinalSection.Name);
DstSection.Texture = nullptr;
DstSection.DrawOffset = NumIndices * sizeof(IndexType);
IndexType* Index = Mesh->mIndexDataSize ? (IndexType*)Mesh->mIndexData + NumIndices : nullptr;
+2 -2
View File
@@ -279,7 +279,7 @@ void lcScene::DrawOpaqueMeshes(lcContext* Context, bool DrawLit, int PrimitiveTy
}
else
{
lcTexture* const Texture = Section->Texture;
lcTexture* const Texture = Section->Texture.Get();
if (Texture)
{
@@ -392,7 +392,7 @@ void lcScene::DrawTranslucentMeshes(lcContext* Context, bool DrawLit, bool DrawF
break;
}
lcTexture* const Texture = Section->Texture;
lcTexture* const Texture = Section->Texture.Get();
int VertexBufferOffset = Mesh->mVertexCacheOffset != -1 ? Mesh->mVertexCacheOffset : 0;
const int IndexBufferOffset = Mesh->mIndexCacheOffset != -1 ? Mesh->mIndexCacheOffset : 0;
+11
View File
@@ -25,6 +25,17 @@ void lcTextureManager::AddTextureReference(lcTexture* Texture)
Texture->AddRef();
}
lcTexture* lcTextureManager::FindTexture(const lcFileSource& Source)
{
QMutexLocker Lock(&mMutex);
for (lcTexture* Texture : mTextures)
if (Texture->mSource == Source)
return Texture;
return nullptr;
}
lcTexture* lcTextureManager::AcquireTexture(const char* TextureName, const lcFileSource& Source, const QString& ProjectPath)
{
if (!TextureName[0] || strlen(TextureName) >= LC_TEXTURE_NAME_LEN)
+3
View File
@@ -19,6 +19,9 @@ public:
// Stop workers and release mesh references before calling.
void Clear();
// Returns a borrowed registered texture for this source, or nullptr. No creation or acquisition.
lcTexture* FindTexture(const lcFileSource& Source);
// Returns an existing texture for this source or creates one.
// Call on the UI thread and release the returned reference when done.
// Returns nullptr for an invalid name or missing source.
+3 -8
View File
@@ -1447,25 +1447,20 @@ lcResult<void> Project::ValidateMergedTextureBindings(const Project& Source, con
for (const std::string& Name : References)
{
lcFileSource BeforeSource;
lcTexture* Before = Library->FindTextureDeferred(Name.c_str(), BeforeDirectories, Source.mEmbeddedDataIndex, BeforeSource);
lcTextureRef Before = Library->AcquireTexture(Name.c_str(), BeforeDirectories, Source.mEmbeddedDataIndex, BeforeSource);
if (!Before)
continue;
lcFileSource AfterSource;
lcTexture* After = Library->FindTextureDeferred(Name.c_str(), AfterDirectories, MergedData, AfterSource);
bool Same = After != nullptr;
lcTextureRef After = Library->AcquireTexture(Name.c_str(), AfterDirectories, MergedData, AfterSource);
bool Same = bool(After);
if (Same && BeforeSource.EmbeddedData && AfterSource.EmbeddedData)
Same = SameEmbeddedData(*BeforeSource.EmbeddedData, *AfterSource.EmbeddedData);
else if (Same)
Same = BeforeSource == AfterSource;
Library->ReleaseTexture(Before);
if (After)
Library->ReleaseTexture(After);
if (!Same)
return lcUnexpected(tr("Merging would change texture '%1' used by model '%2'. Rename the conflicting texture before merging.").arg(QString::fromLatin1(Name.c_str()), Model->GetFileName()));
}
+29 -14
View File
@@ -88,27 +88,33 @@ source snapshots and the library's build services; they do not consult the
active project or publish into live models.
The texture manager is owned by the library and owns the only texture registry.
`FindTextureDeferred()` resolves embedded data, project-directory files, and
`AcquireTexture()` resolves embedded data, project-directory files, and
indexed library files in the library, then asks the manager to acquire an identity
for that resolved source. The lookup returns the source snapshot alongside the
acquired identity so callers can pass it to loader requests without reading texture fields.
for that resolved source. The explicit-search acquisition overload returns the
source snapshot alongside the scoped `lcTextureRef`, so callers can pass it to
loader requests without reading texture fields.
Resolution preserves the prefixed search order and
canonicalizes disk paths. The project directory is passed separately to acquisition
so the manager preserves temporary-texture metadata without searching files itself.
Library texture APIs forward reference changes and decoding to the manager.
`FindTexture()` still gathers the explicit project's context and waits through
the loader. Texture requests,
The project overload of `AcquireTexture()` gathers the explicit project's context
without starting decoding or waiting. `FindTexture(Source)` only borrows an already
registered texture by its resolved source; it performs no source search or creation.
`RequestTextureLoad()` starts or promotes work, and `WaitForTexture()` explicitly
waits through decoding and upload. Texture requests,
priority promotion, cancellation, upload/retries, and part dependency settlement
remain in `lcAssetLoader`; its in-flight texture references are acquired through
the manager. Shared readiness, priority, file-source and piece-source snapshots,
build results, and `lcTextureLoadError` live in `lc_assettypes.h`. Stud-style and
remain in `lcAssetLoader`; its requests retain `lcTextureRef` handles, explicitly
reset on the UI thread before worker-shared request storage can be destroyed.
Shared readiness, priority, file-source and piece-source snapshots, build results, and `lcTextureLoadError` live in `lc_assettypes.h`. Stud-style and
archive-type enums remain in `lc_library.h` and are forward-declared by the shared
header; the piece snapshot constructor supplies their defaults out of line.
`lcTexture::mSource` stores the source identity; copies for workers do not snapshot
file contents. Texture names, project directories, and temporary flags remain separate.
Callers pass the resolved source into each texture request. The library's
`BuildTextureData()` and `AddTextureReference()` are internal loader services,
accessible through the existing `lcAssetLoader` friendship.
Loader dependency requests receive the resolved source explicitly. Direct library
request/wait wrappers copy the texture's source on the UI thread. The loader uses
the library's internal `BuildTextureData()` service through friendship; scoped
references use its internal `AddTextureReference()` and `ReleaseTexture()`
primitives.
The library calls `OpenFile()` on the reading thread and passes the opened
`lcFile&` to the texture manager for decoding. `BuildTextureData()` continues to
@@ -397,6 +403,15 @@ are safe. `==` and `!=` compare pointer identity between handles or with a
borrowed pointer in either order, including null, without changing reference counts.
Use `Get()` when passing a borrowed pointer to an API.
Textures use the same lookup/acquisition convention: `FindTexture(Source)` borrows
a registered identity and `AcquireTexture()` returns a scoped reference without
loading. `RequestTextureLoad()` only schedules work; `WaitForTexture()` explicitly
waits through decoding and upload and accepts `const lcTextureRef&`, matching
the piece wait API. Both require the caller to retain the texture.
Mesh texture sections own `lcTextureRef` handles, so section copies retain and
moves transfer ownership. The library's add/release primitives are internal to
scoped reference management.
`lcPiece::SetPieceData()` accepts an `lcPieceDataRef` by value, adopts that
reference, and releases the previous identity. Passing an lvalue copies and retains;
passing a moved handle transfers the reference. Its raw-pointer constructor is a
@@ -516,16 +531,16 @@ adds strict failure reporting. During shutdown, the loader's private
| Need | API and lifetime contract |
| --- | --- |
| Find a registered identity | `FindPieceData(Name, Project)` returns a borrowed identity without I/O or creation; retain it before keeping it beyond its owner's lifetime. |
| Find a registered identity | `FindPieceData(Name, Project)` or `FindTexture(Source)`. Borrowed lookup only, with no I/O or creation. |
| Resolve or create a piece identity | `AcquirePieceData(Name, Project, CreateMissing, SearchProjectFolder)` returns a scoped reference without ensuring mesh readiness. |
| Start ordinary asynchronous work | Retain the identity, then call `RequestPieceDataLoad()` with `lcLoadPriority::Low` for background work or `Normal` for model/view work. Existing owners need no extra reference for another request. |
| Retain and synchronously load one identity | Keep an `lcPieceDataRef` from `AcquirePieceData()`, then call `WaitForPieceData(PieceDataRef)` and check its return value. |
| Retain without starting work | `lcPieceDataRef(Piece)` returns a scoped reference. This does not make geometry ready. |
| Retain without starting work | `lcPieceDataRef(Piece)` or `lcTextureRef(Texture)` returns a scoped reference to an existing identity. This does not make the asset ready. |
| Wait for an explicit part set | `WaitForPieceData(PieceDataRefs)` (use `WaitForPieceData(PieceDataRef)` for one identity), check the boolean for strict consumers. Retain every identity and its owner throughout the wait; this call acquires no consumer references. |
| Wait for one identity and its container geometry | `EnsurePieceDataAssetsReady()` settles the identity, then delegates recursive children, synths, and bounds to the model/project API. Retain the identity and its owner throughout. |
| Wait for a model, including nested synths | `WaitForAssets()` for permissive output; `EnsureAssetsReady()` for strict preview-style failure reporting. Keep the model and its pieces alive throughout. |
| Replace/wait for generated geometry | `RequestSynthMesh()` / `WaitForSynthMeshes()`. Supply live `lcPiece` instances; a shared part mesh or a retained previous generated mesh is not readiness for the replacement. |
| Acquire a texture directly | `FindTexture()` waits for readiness and returns an acquired texture or null. `FindTextureDeferred()` acquires without waiting. The loader handles GPU upload readiness internally. Retain the texture during a direct wait and pair acquisition with `ReleaseTexture()`. |
| Acquire a texture directly | Keep an `lcTextureRef` from `AcquireTexture()`. Use `RequestTextureLoad()` to start asynchronous work or `WaitForTexture()` to ensure readiness. Acquisition alone does not decode or upload. |
Scoped waits may process unrelated completions while pumping publication, but
their completion condition depends only on the requested assets. They cannot