mirror of
https://github.com/leozide/leocad.git
synced 2026-10-26 22:10:32 +00:00
Document asset loading contracts and clarify queue helper naming
This commit is contained in:
1 parent
622774f254
commit
145b3238db
4 files changed
+521
-11
No files matched your search
@@ -34,7 +34,7 @@ bool lcAssetLoader::LoadPieceInfo(PieceInfo* Info, lcPieceLoadFlags Flags)
|
||||
if (Info->IsProject() || (Info->IsModel() && !Info->GetModel()->HasDirectGeometry()))
|
||||
Info->mState = lcPieceInfoState::Loaded;
|
||||
else if (Info->mState == lcPieceInfoState::Unloaded)
|
||||
QueuePieceLocked(Info, Flags.testFlag(lcPieceLoadFlag::Visible) ? Priority::Visible : Priority::Background);
|
||||
EnqueuePiece(Info, Flags.testFlag(lcPieceLoadFlag::Visible) ? Priority::Visible : Priority::Background);
|
||||
else if (Flags.testFlag(lcPieceLoadFlag::Visible))
|
||||
{
|
||||
mQueueMutex.lock();
|
||||
@@ -51,11 +51,12 @@ void lcAssetLoader::QueuePiece(PieceInfo* Info, bool PriorityHint)
|
||||
{
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
QueuePieceLocked(Info, PriorityHint ? Priority::Visible : Priority::Background);
|
||||
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);
|
||||
@@ -239,6 +240,7 @@ void lcAssetLoader::PromoteRequestPriorityLocked(const std::shared_ptr<Request>&
|
||||
|
||||
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))
|
||||
@@ -286,7 +288,7 @@ void lcAssetLoader::OnConsumerReleased(PieceInfo* Info)
|
||||
mLibrary->ReleasePieceLoadHold(Info);
|
||||
}
|
||||
|
||||
void lcAssetLoader::QueuePieceLocked(PieceInfo* Info, Priority LoadPriority)
|
||||
void lcAssetLoader::EnqueuePiece(PieceInfo* Info, Priority LoadPriority)
|
||||
{
|
||||
QMutexLocker QueueLock(&mQueueMutex);
|
||||
|
||||
@@ -857,6 +859,7 @@ void lcAssetLoader::WaitForResultsLocked()
|
||||
|
||||
bool lcAssetLoader::ProcessCompletions()
|
||||
{
|
||||
// Bounded UI-thread publication also runs during scoped waits, without public callbacks.
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
bool Progress = false;
|
||||
@@ -1243,6 +1246,7 @@ bool lcAssetLoader::ProcessCompletions()
|
||||
|
||||
void lcAssetLoader::DispatchNotifications()
|
||||
{
|
||||
// Event-handler delivery avoids reentering consumers inside a synchronous readiness wait.
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
constexpr int MaxNotificationsPerPump = 32;
|
||||
@@ -1379,7 +1383,7 @@ bool lcAssetLoader::EnsurePiecesReady(const std::vector<PieceInfo*>& Parts)
|
||||
if (Info->IsProject() || (Info->IsModel() && !Info->GetModel()->HasDirectGeometry()))
|
||||
Info->mState = lcPieceInfoState::Loaded;
|
||||
else
|
||||
QueuePieceLocked(Info, Priority::Blocking);
|
||||
EnqueuePiece(Info, Priority::Blocking);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1456,7 +1460,7 @@ void lcAssetLoader::QueueModelPiece(PieceInfo* Info)
|
||||
|
||||
Info->mState = lcPieceInfoState::Unloaded;
|
||||
|
||||
QueuePieceLocked(Info, Priority::Visible);
|
||||
EnqueuePiece(Info, Priority::Visible);
|
||||
}
|
||||
|
||||
void lcAssetLoader::InvalidatePiece(PieceInfo* Info)
|
||||
@@ -1547,6 +1551,7 @@ bool lcAssetLoader::HasPendingWork()
|
||||
|
||||
void lcAssetLoader::CancelAndDrain()
|
||||
{
|
||||
// Shutdown/reload joins obsolete workers and releases holds without requiring GL success.
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
std::deque<std::shared_ptr<Request>> Cancelled;
|
||||
|
||||
+27
-5
@@ -13,6 +13,7 @@ class Image;
|
||||
class lcTexture;
|
||||
enum class lcTextureLoadError;
|
||||
|
||||
// Copied on the UI thread so workers do not consult live models or project lookup.
|
||||
struct lcPartSourceSnapshot
|
||||
{
|
||||
QString FileName;
|
||||
@@ -28,6 +29,7 @@ struct lcPartSourceSnapshot
|
||||
bool StudCylinderColorEnabled = false;
|
||||
};
|
||||
|
||||
// Private worker output; meshes and dependencies are published only on the UI thread.
|
||||
struct lcPartBuildResult
|
||||
{
|
||||
lcPartBuildResult() = default;
|
||||
@@ -67,6 +69,9 @@ struct lcTextureBuildResult
|
||||
lcTextureLoadError Error{};
|
||||
};
|
||||
|
||||
// Owned by lcPiecesLibrary. UI-thread entry points schedule private worker builds;
|
||||
// UI-thread completion processing resolves colors, uploads textures, and publishes meshes.
|
||||
// See docs/asset-loading.md for readiness, ownership, and caller policies.
|
||||
class lcAssetLoader : public QObject
|
||||
{
|
||||
public:
|
||||
@@ -101,6 +106,8 @@ private:
|
||||
Blocking // Assets needed by a synchronous wait.
|
||||
};
|
||||
|
||||
// Owns one PieceInfo load hold through completion and deferred notification.
|
||||
// Parsing and conversion are separate worker phases; textures settle before publication.
|
||||
struct Request
|
||||
{
|
||||
PieceInfo* Info;
|
||||
@@ -109,10 +116,10 @@ private:
|
||||
lcPartSourceSnapshot Source;
|
||||
Priority LoadPriority;
|
||||
qint64 EnqueuedAt;
|
||||
bool Running = false;
|
||||
bool Obsolete = false;
|
||||
bool Terminal = false;
|
||||
bool Succeeded = false;
|
||||
bool Running = false; // A worker phase 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.
|
||||
bool ConvertingMesh = false;
|
||||
std::unique_ptr<lcLibraryMeshData> MeshData;
|
||||
std::vector<bool> ColorTranslucency;
|
||||
@@ -123,6 +130,7 @@ private:
|
||||
QString Error;
|
||||
};
|
||||
|
||||
// Holds a texture reference through CPU decoding and UI-thread upload/retries.
|
||||
struct TextureRequest
|
||||
{
|
||||
lcTexture* Texture;
|
||||
@@ -135,6 +143,8 @@ private:
|
||||
qint64 RetryAt = 0;
|
||||
};
|
||||
|
||||
// The instance cancels before destruction; workers use the copied definition/controls.
|
||||
// Cancelled is also checked during expensive generation and conversion.
|
||||
struct SynthRequest
|
||||
{
|
||||
lcPiece* Piece;
|
||||
@@ -158,6 +168,7 @@ private:
|
||||
using TextureRequestMap = std::map<lcTexture*, std::shared_ptr<TextureRequest>>;
|
||||
using SynthRequestMap = std::map<lcPiece*, std::shared_ptr<SynthRequest>>;
|
||||
|
||||
// Workers hand off one private result here; no live asset is published by a worker.
|
||||
struct Completion
|
||||
{
|
||||
std::shared_ptr<Request> RequestedPart;
|
||||
@@ -169,6 +180,7 @@ private:
|
||||
std::unique_ptr<lcMesh> SynthMesh;
|
||||
};
|
||||
|
||||
// UI-thread delivery retains the load hold after publication, until callbacks finish.
|
||||
struct Notification
|
||||
{
|
||||
std::shared_ptr<Request> RequestedPart;
|
||||
@@ -176,17 +188,21 @@ private:
|
||||
QString Error;
|
||||
};
|
||||
|
||||
// Retains the base identity; cancellation makes delivery safe after instance deletion.
|
||||
struct SynthNotification
|
||||
{
|
||||
std::shared_ptr<SynthRequest> RequestedSynth;
|
||||
PieceInfo* Info;
|
||||
};
|
||||
|
||||
void QueuePieceLocked(PieceInfo* Info, Priority LoadPriority);
|
||||
// Acquires mQueueMutex internally; callers must not already hold it.
|
||||
void EnqueuePiece(PieceInfo* Info, Priority LoadPriority);
|
||||
// Caller holds mQueueMutex.
|
||||
void PromoteRequestPriorityLocked(const std::shared_ptr<Request>& RequestedPart, Priority LoadPriority);
|
||||
void QueueTexture(lcTexture* Texture, Priority LoadPriority);
|
||||
static QString TextureFailureMessage(const lcTexture* Texture);
|
||||
void CancelUnusedTextureRequests();
|
||||
// Caller holds mQueueMutex.
|
||||
void StartWorkersLocked();
|
||||
void LoadQueuedPieces();
|
||||
bool ProcessCompletions();
|
||||
@@ -197,17 +213,22 @@ private:
|
||||
void FinishSynth(const std::shared_ptr<SynthRequest>& RequestedSynth);
|
||||
void UploadTextureRequest(const std::shared_ptr<TextureRequest>& RequestedTexture);
|
||||
void FinishTextureRequest(const std::shared_ptr<TextureRequest>& RequestedTexture);
|
||||
// Caller holds mQueueMutex; the condition wait releases it and reacquires it on return.
|
||||
void WaitForResultsLocked();
|
||||
bool WaitForRequest(PieceInfo* Info);
|
||||
bool event(QEvent* Event) override;
|
||||
|
||||
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;
|
||||
// Serializes full queue drains; scoped readiness waits do not acquire this mutex.
|
||||
QMutex mDrainMutex;
|
||||
PartRequestMap mRequests;
|
||||
TextureRequestMap mTextureRequests;
|
||||
SynthRequestMap mSynthRequests;
|
||||
// UI-thread diagnostics and instance tracking; these do not own the pieces.
|
||||
std::unordered_map<const lcPiece*, QString> mFailedSynthErrors;
|
||||
std::unordered_set<lcPiece*> mSynthPieces;
|
||||
std::deque<std::shared_ptr<Request>> mQueue;
|
||||
@@ -219,6 +240,7 @@ private:
|
||||
std::deque<Completion> mCompletions;
|
||||
std::deque<std::shared_ptr<Request>> mStarted;
|
||||
std::deque<std::shared_ptr<TextureRequest>> mStartedTextures;
|
||||
// UI-thread only; public signals are deferred until outside synchronous waits.
|
||||
std::deque<Notification> mNotifications;
|
||||
std::deque<SynthNotification> mSynthNotifications;
|
||||
std::vector<QFuture<void>> mFutures;
|
||||
|
||||
+29
-1
@@ -150,6 +150,9 @@ struct lcLibrarySource
|
||||
std::map<std::string, lcLibraryPrimitive*> Primitives;
|
||||
};
|
||||
|
||||
// Owns catalog/local identities, sources, references, and shared render buffers.
|
||||
// Its asset loader owns pending work; workers build privately and the UI thread publishes.
|
||||
// See docs/asset-loading.md for the loading and lifetime contracts.
|
||||
class lcPiecesLibrary : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -179,37 +182,58 @@ public:
|
||||
void SetModelPieceName(PieceInfo* Info, const char* Name);
|
||||
bool RenamePiece(PieceInfo* Info, const char* NewName);
|
||||
static std::string NormalizePieceName(const char* PieceName);
|
||||
// Returns one acquired model reference, paired with ReleasePieceInfo().
|
||||
PieceInfo* CreateModelPiece(const char* PieceName, Project* Project, bool& Reused);
|
||||
// Returns a borrowed identity; lookup does not acquire a reference or ensure readiness.
|
||||
PieceInfo* FindPiece(const char* PieceName, Project* Project, bool CreateMissing, bool SearchProjectFolder);
|
||||
bool RemapProjectPiece(PieceInfo* Info, const QString& ProjectDirectory, bool IsPreview);
|
||||
|
||||
// Asset scheduling, waits, invalidation, and consumer releases below run on the UI thread.
|
||||
// Acquires one reference even on failure; always pair with ReleasePieceInfo().
|
||||
// Without Wait, true does not establish readiness. See docs/asset-loading.md.
|
||||
bool LoadPieceInfo(PieceInfo* Info, lcPieceLoadFlags Flags);
|
||||
void NotifyConsumersChanged();
|
||||
// Wait for these identities only, with temporary holds; false means failure/cancellation.
|
||||
// Does not traverse container children or wait for per-instance synth geometry.
|
||||
bool EnsurePieceReady(PieceInfo* Info);
|
||||
bool EnsurePiecesReady(const std::vector<PieceInfo*>& Parts);
|
||||
// Replaces any pending generation for this instance; snapshots control points for workers.
|
||||
void QueueSynthMesh(lcPiece* Piece);
|
||||
void CancelSynthMesh(lcPiece* Piece);
|
||||
// Caller keeps the instances alive through the wait; false means missing generated geometry.
|
||||
bool EnsureSynthMeshesReady(const std::vector<lcPiece*>& Pieces);
|
||||
QString GetSynthMeshError(const lcPiece* Piece) const;
|
||||
void QueueModelPiece(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.
|
||||
bool EnsureTextureReady(lcTexture* Texture);
|
||||
// Releases one reference and lets the loader cancel unused queued work.
|
||||
void ReleasePieceInfo(PieceInfo* Info);
|
||||
// Lifetime hold only: does not start loading or establish readiness.
|
||||
void AddPieceReference(PieceInfo* Info);
|
||||
// Loader helper: assumes a pending request/notification owns one in-flight hold.
|
||||
bool HasPieceConsumers(const PieceInfo* Info);
|
||||
// Releases that in-flight hold without triggering consumer-release cancellation.
|
||||
void ReleasePieceLoadHold(PieceInfo* Info);
|
||||
void SetPieceLoadError(const PieceInfo* Info, QString Error);
|
||||
QString GetPieceLoadError(const PieceInfo* Info) const;
|
||||
void ClearPieceLoadError(const PieceInfo* Info);
|
||||
bool LoadBuiltinPieces();
|
||||
lcPartSourceSnapshot 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);
|
||||
// UI-thread drain of all work, including unrelated assets; not a success check.
|
||||
void WaitForLoadQueue();
|
||||
|
||||
// Returns a texture reference that the caller must release.
|
||||
// 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, without decoding or upload; caller releases the reference.
|
||||
lcTexture* FindTextureDeferred(const char* TextureName, const std::vector<QString>& SearchDirectories);
|
||||
lcTextureSourceSnapshot SnapshotTextureSource(const lcTexture* Texture) const;
|
||||
// Worker decode service: returns private pixels/errors without GL upload or publication.
|
||||
lcTextureBuildResult BuildTextureData(const lcTextureSourceSnapshot& Source);
|
||||
bool LoadTexture(lcTexture* Texture);
|
||||
void ReleaseTexture(lcTexture* Texture);
|
||||
@@ -302,9 +326,13 @@ protected:
|
||||
// project for each local PieceInfo, or nullptr after that project is destroyed.
|
||||
std::unordered_map<PieceInfo*, Project*> mProjectPieces;
|
||||
|
||||
// Protects identity/reference bookkeeping and shared primitive loading state.
|
||||
// Recursive because library release and lookup paths can reenter this lock.
|
||||
lcLibraryLoadMutex mLoadMutex;
|
||||
|
||||
// Serializes texture source access, including archive extraction by workers.
|
||||
QMutex mTextureMutex;
|
||||
// Sparse errors avoid storing a QString in every catalog identity.
|
||||
mutable QMutex mPieceErrorMutex;
|
||||
std::unordered_map<const PieceInfo*, QString> mFailedPartErrors;
|
||||
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
# Asset loading
|
||||
|
||||
This guide describes the current implementation. Update it with changes to
|
||||
loading, ownership, readiness, scheduling, or output policy. Names below refer
|
||||
to existing APIs, rather than proposed interfaces.
|
||||
|
||||
This is the implementation record replacing the streaming-assets plan. It
|
||||
documents the current contracts and accepted decisions, rather than retaining
|
||||
completed stages or superseded proposals as future requirements.
|
||||
|
||||
## What users see
|
||||
|
||||
Opening a model still parses its file on the UI thread, but does not wait for
|
||||
every referenced mesh or texture. Unavailable geometry initially draws as cubes;
|
||||
pieces appear incrementally once their meshes **and textures** are ready.
|
||||
Already-loaded shared assets can appear immediately. A disk cache hit still
|
||||
requires worker loading and any texture dependencies; it is not a UI-thread fast
|
||||
path and receives no special queue priority.
|
||||
|
||||
The status-bar spinner is visible while the active project has pending pieces
|
||||
or generated meshes. Its tooltip reports loading counts, or regeneration when
|
||||
only synth work remains. Counts describe unique required `PieceInfo` identities
|
||||
plus generated piece instances, not the number of placed bricks or global jobs.
|
||||
Requirements include all project models and their nested dependencies, not just
|
||||
the visible step. The spinner hides immediately when work settles; there is no
|
||||
"all pieces loaded" message. A separate failure indicator retains error details
|
||||
until the project's failures disappear or the project changes.
|
||||
|
||||
Views redraw and update bounds as results arrive, but keep the user's camera.
|
||||
During synth edits, the previous generated mesh stays visible until a replacement
|
||||
settles. Images and geometry exports wait for their requirements and omit failed
|
||||
geometry instead of saving loading cubes. See [Caller policies](#caller-policies)
|
||||
for the deliberately stricter preview rules and thumbnail fallbacks.
|
||||
|
||||
## Accepted decisions
|
||||
|
||||
These decisions are the baseline, not open TODO items. Do not propose reversing
|
||||
them merely because another design is possible or an older requirement differs.
|
||||
Revisit one only at the user's request or when a concrete regression, changed
|
||||
requirement, or relevant measurement invalidates its rationale. Explain that
|
||||
new evidence and update this record with any agreed change.
|
||||
|
||||
| Decision | Rationale and alternatives set aside |
|
||||
| --- | --- |
|
||||
| Keep identities, sources, references, and shared buffers in the library; pending work belongs to its loader. | One registry and one completion path avoid competing lifetime/readiness authorities. Do not create a second asset registry in the loader. |
|
||||
| Use the existing Qt worker pool and shared offscreen GL context, with UI-thread publication. | Reuses existing infrastructure and keeps model mutation, color registration, upload, and rendering sequential. Do not publish assets from workers or introduce a separate renderer/context for loading. |
|
||||
| Use promotion-only priorities and existing reference counts. | Models open at their last step; precise consumer demotion added bookkeeping for little demonstrated benefit. Per-consumer pointer maps, step-based priorities, outgoing-project demotion, and scoped priority removal were deliberately set aside. Accept briefly elevated work after a requesting view closes. |
|
||||
| Treat missing geometry as `Failed`; keep structural type and library membership separate. | A separate Placeholder/Missing type or readiness value duplicates failure state. Cubes are shared display fallbacks, not stored asset meshes. Library membership is established explicitly by the `PieceInfo` constructor. |
|
||||
| Require all includes and textures for successful asset loading; never cache an erroneous mesh as successful. | Partial private builds must not masquerade as ready assets. Permissive output is a caller policy that omits failed assets, not relaxed loader success criteria. |
|
||||
| Allow model outputs to save available geometry after waiting. | Geometry exports, HTML, images, instructions, and printing do not reject the whole operation because an asset failed. Actual rendering/file failures remain errors. Strict previews and thumbnail fallback rules remain separate. |
|
||||
| Keep missing HTML parts-list entries with empty thumbnails and quantities. | Missing geometry must not erase the inventory entry or substitute a loading cube. Instruction parts lists currently omit unavailable thumbnails. |
|
||||
| Fit outputs to available geometry; interactive fitting includes fallbacks. | Failed cubes must not distort exported framing. Include root direct geometry and available children, preserve explicit cameras, and never automatically refit interactive views as assets arrive. |
|
||||
| Build synth meshes per instance on workers; retain the previous generated mesh during edits and cancel obsolete work cooperatively. | Control points belong to placements, not shared library assets. Keep interactive edits stable while ensuring output never mistakes an older mesh for the completed replacement. |
|
||||
| Keep the uncached mesh draw path and debounced shared-buffer repacking. | Measured repacking cost was small and repeated scenes benefited from shared buffers. Per-mesh buffers, visibility-triggered uploads, and mesh-buffer upload as a readiness requirement were set aside without evidence of a benefit. Texture upload remains part of readiness. |
|
||||
|
||||
## Responsibilities and entry points
|
||||
|
||||
| Owner | Responsibility | Source |
|
||||
| --- | --- | --- |
|
||||
| `lcPiecesLibrary` | Asset identities, references, source discovery, archives, primitive and mesh caches, settings, shared mesh buffers, and public forwarding APIs. | [lc_library.h](../common/lc_library.h), [lc_library.cpp](../common/lc_library.cpp) |
|
||||
| `lcAssetLoader` | Pending requests, worker scheduling, private results, texture upload, publication, cancellation, readiness waits, and deferred settlement notifications. | [lc_assetloader.h](../common/lc_assetloader.h), [lc_assetloader.cpp](../common/lc_assetloader.cpp) |
|
||||
| `PieceInfo` / `lcTexture` | Published geometry or texture data, readiness state, and asset metadata. | [pieceinf.h](../common/pieceinf.h), [lc_texture.h](../common/lc_texture.h) |
|
||||
| Projects, models, and views | Project-local names, required-asset traversal, generated-piece ownership, bounds, fallback display, cameras, and redraws. | [project.cpp](../common/project.cpp), [lc_model.cpp](../common/lc_model.cpp), [piece.cpp](../common/piece.cpp) |
|
||||
|
||||
The library owns the loader. Call library entry points from the UI thread for
|
||||
asset acquisition, release, scheduling, waiting, and invalidation. Workers use
|
||||
source snapshots and the library's build services; they do not consult the
|
||||
active project or publish into live models.
|
||||
|
||||
`FindPiece()` returns a borrowed identity, without acquiring a reference. Lookup
|
||||
checks the supplied project's name index, then the shared library catalog, then
|
||||
optionally an external file beside the project. Creating a missing identity
|
||||
records `Failed`. `CreateModelPiece()` differs: it returns one acquired reference
|
||||
for the model to release. Library membership is independent of readiness and
|
||||
Part/Model/Project type. Project-local names cannot replace another project's
|
||||
names. Worker include resolution uses library sources, while model geometry and
|
||||
texture search directories are snapshotted before dispatch.
|
||||
|
||||
### Project-local identities and source paths
|
||||
|
||||
`Project::mPieceIndex` maps normalized local filenames to borrowed `PieceInfo`
|
||||
pointers. "Project-local names" means lookup visibility, not object ownership:
|
||||
the library tracks local identities separately in `mProjectPieces`, and reference
|
||||
counts govern their lifetime. Models know their owning project from construction;
|
||||
temporary models can have no project. Do not use the active project implicitly
|
||||
when resolving a candidate project, external file, or preview.
|
||||
|
||||
This scope lets different open/candidate/preview projects use the same submodel
|
||||
name without replacing one another's identities. Containers remain structurally
|
||||
Model or Project even when their direct mesh fails. The shared catalog remains
|
||||
separate; worker primitive/part includes must not accidentally resolve through
|
||||
a project's name index.
|
||||
|
||||
Source directories are also distinct from the project's current save filename.
|
||||
Models preserve asset search directories when moving between projects or saving
|
||||
to a new folder, so embedded geometry can still find its source textures. Save As
|
||||
uses `QSaveFile`; source state and the old filename are restored if writing or
|
||||
commit fails. After a successful folder change, local external references are
|
||||
remapped in place against the destination folder and direct model meshes are
|
||||
invalidated/requeued. Missing replacements become failed identities and retain
|
||||
their references. Saving in the same folder skips this remapping.
|
||||
|
||||
Merge validates name/identity conflicts before mutating either project, renames
|
||||
conflicting submodels, transfers local registrations, and preserves source
|
||||
directories. Renames update live instance IDs and undo/redo snapshots. Deletion
|
||||
and project teardown unregister borrowed index entries; they must not leave
|
||||
lookup pointers to deleted objects. These paths are in
|
||||
[project.cpp](../common/project.cpp) and [lc_model.cpp](../common/lc_model.cpp).
|
||||
|
||||
## Readiness
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Part["PieceInfo request"]
|
||||
U[Unloaded] --> Q[Queued] --> L[Loading]
|
||||
L --> A[AwaitingTextures] --> R[Loaded]
|
||||
L --> R
|
||||
L --> F[Failed]
|
||||
A --> F
|
||||
Q --> C[Cancelled]
|
||||
end
|
||||
subgraph Texture["Texture request"]
|
||||
TU[Unrequested] --> TQ[Queued] --> TD[Decoding]
|
||||
TD --> TC[Decoded] --> TR[Ready]
|
||||
TD --> TF[Failed]
|
||||
TC --> TP[RetryPending] --> TC
|
||||
TC --> TF
|
||||
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.
|
||||
|
||||
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
|
||||
include or declared texture is a failure, even if some geometry was built.
|
||||
Texture names remain in mesh sections and cache data when lookup fails.
|
||||
|
||||
A texture is ready only when `IsReady()` reports `Ready` with a nonzero GL
|
||||
texture. CPU pixels, `HasImageData()`, and `!NeedsUpload()` do not establish
|
||||
readiness. The completion pump uploads through the global shared offscreen
|
||||
context without requiring the part to be drawn. Upload failures retry at most
|
||||
three times, with 50 ms and 200 ms delays; decoding failures settle immediately.
|
||||
Loader-decoded textures receive `LC_TEXTURE_MIPMAPS`.
|
||||
|
||||
**Container readiness is not recursive readiness.** A Project `PieceInfo`, or a
|
||||
Model without direct geometry, can be `Loaded` while its children are pending
|
||||
or failed. Models with directly embedded geometry have their own mesh request.
|
||||
Use `lcModel::GetRequiredPieces()` and `GetRequiredSynthPieces()` for recursive
|
||||
requirements, including nested and external models, with cycle protection.
|
||||
|
||||
### Diagnostics and retries
|
||||
|
||||
Part error text lives in the library's sparse `mFailedPartErrors` map, queried
|
||||
through `GetPieceLoadError()`, rather than an empty string in every catalog part.
|
||||
Synth error text is likewise sparse in the loader and available through
|
||||
`GetSynthMeshError()`. Entries must be cleared when their identity is unloaded,
|
||||
deleted, invalidated, or successfully rebuilt; synth cancellation also clears
|
||||
its entry. Pointer-keyed errors must not survive destruction and later attach
|
||||
to a different object allocated at the same address.
|
||||
|
||||
Textures store a compact `lcTextureLoadError` enum. The loader's static
|
||||
`TextureFailureMessage()` translates it into user-facing text when reporting a
|
||||
dependency failure. Missing includes and texture names remain in diagnostics;
|
||||
an empty error string alone does not prove readiness. Preserve translatable
|
||||
messages and check state/result rather than parsing error text.
|
||||
|
||||
Only GPU upload attempts receive the bounded automatic retry described
|
||||
above. Missing files, invalid source geometry, and decode failures do not produce
|
||||
an endless render-time retry loop. A strict caller reports failure; a permissive
|
||||
caller omits unavailable geometry, while the loading indicator exposes errors.
|
||||
|
||||
## A request from acquisition to notification
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Caller as Caller / UI thread
|
||||
participant Library as Library
|
||||
participant Loader as Loader / UI thread
|
||||
participant Worker as Qt worker
|
||||
Caller->>Library: LoadPieceInfo(Info, Flags)
|
||||
Library->>Loader: Acquire reference and 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: Resolve textures and queue decode if needed
|
||||
Loader->>Worker: Texture source snapshot
|
||||
Worker-->>Loader: Decoded CPU image
|
||||
Loader->>Loader: Upload texture and commit ready mesh
|
||||
Loader-->>Caller: Deferred settlement notification
|
||||
```
|
||||
|
||||
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 on workers; 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 as successful mesh
|
||||
caches. Temporary disk textures are scoped by their resolved file identity, so
|
||||
same-name PNGs in different project directories remain distinct.
|
||||
|
||||
Mesh cache validity includes the cache format, source checksums, stud style, and
|
||||
stud-cylinder color setting. Style flags propagate through nested primitive
|
||||
includes. `LoadPrimitive()` returns `lcResult<void>` with include errors and
|
||||
clears partial data on failure, so failed primitive builds cannot seed a later
|
||||
successful mesh with incomplete cached geometry.
|
||||
|
||||
Workers post results and wake the loader's completion event. `ProcessCompletions()`
|
||||
processes at most 16 completions per pump, including texture retries. Publication
|
||||
and rendering remain sequential on the UI thread. `DispatchNotifications()`
|
||||
later delivers `PartLoaded`, `PartLoadFailed`, and `GeneratedMeshSettled`, with
|
||||
bounded batches and stale-target checks. Synchronous waits drive completion
|
||||
processing but defer these public settlement notifications to the event handler;
|
||||
they do not run an unrestricted nested UI event loop. `AssetRequestsChanged`
|
||||
also signals request-list or consumer changes and is not a success notification.
|
||||
|
||||
The main window coalesces updates, propagates affected bounds, and redraws views.
|
||||
Its loading indicator counts the active project's requirements, including
|
||||
generated meshes; unrelated work does not keep that indicator visible. Mesh
|
||||
publication schedules a debounced shared-buffer repack after loader work becomes
|
||||
quiescent. Until then, meshes draw through the existing uncached path. Shared
|
||||
buffer upload is not part of asset readiness.
|
||||
|
||||
## References, cancellation, and generated geometry
|
||||
|
||||
`LoadPieceInfo()` acquires one reference even if loading fails. Always pair it
|
||||
with `ReleasePieceInfo()`. Its asynchronous return value means the call did not
|
||||
wait, not that the asset loaded. `AddPieceReference()` retains an identity without
|
||||
starting work. `EnsurePiecesReady()` deduplicates its input, temporarily holds
|
||||
each part, and releases those holds before returning.
|
||||
|
||||
A part request owns an additional in-flight reference. Removing one instance
|
||||
cannot destroy an asset still used by another instance or pending work. On the
|
||||
last consumer release, queued work is removed; running ordinary part work is
|
||||
allowed to finish and its result is discarded if no consumer survives. Successful
|
||||
or failed publication keeps the request's hold through deferred notification,
|
||||
then releases it. Library piece identities can remain in the catalog after their
|
||||
geometry unloads; unreferenced local leaf identities can be deleted. Model and
|
||||
external-project containers have their own teardown rules.
|
||||
|
||||
Texture requests likewise hold a reference, and staged/published mesh sections
|
||||
hold their texture dependencies. Cancellation removes unused queued texture work
|
||||
without releasing another mesh's texture. Failed assets remain terminal while
|
||||
retained; drawing does not retry them. Explicit invalidation or unloading can
|
||||
permit a later request.
|
||||
|
||||
Synth meshes are **per `lcPiece` instance**, separate from the shared library
|
||||
mesh. `QueueSynthMesh()` copies control points and retains the shared synth
|
||||
definition. Replacing or deleting the piece cancels its old request; workers
|
||||
check an atomic cancellation flag and stale results cannot replace newer meshes.
|
||||
Initial generation displays a cube. Control-point edits retain the previous
|
||||
generated mesh until replacement settles; that older mesh does not satisfy a
|
||||
wait for the new generation. Settlement holds the base `PieceInfo` through its
|
||||
deferred notification. See [lc_synth.cpp](../common/lc_synth.cpp) and
|
||||
[piece.cpp](../common/piece.cpp).
|
||||
|
||||
Interactive fallback rendering, bounds, ray/box picking, and selection outlines
|
||||
use the same displayed geometry and keep the original instance identity. A
|
||||
model's unavailable direct geometry can display a local cube while its children
|
||||
remain independently visible/selectable. Failed containers retain their
|
||||
structure; deleting a model or external project detaches its pointers explicitly.
|
||||
Undo restores external references using project-folder lookup, not only the
|
||||
library catalog. Placeholder appearance must never change piece type or asset
|
||||
ownership.
|
||||
|
||||
## Scheduling and waits
|
||||
|
||||
Priorities only promote: `Background < Visible < Blocking`. Ordinary thumbnail
|
||||
requests start Background; asynchronous model pieces and generated meshes start
|
||||
Visible, regardless of step. Synchronous waits promote their required requests
|
||||
and pending texture dependencies to Blocking. Shared textures retain the highest
|
||||
promotion received from any parent or direct wait. There is no consumer identity
|
||||
map, priority demotion, or step-based scheduling. Priority lasts until that request
|
||||
completes or is cancelled, not merely until the requesting view closes.
|
||||
|
||||
At most four QtConcurrent drain workers run (fewer if the machine reports fewer
|
||||
threads). Running work is nonpreemptive. Queued work gains one scheduling priority
|
||||
level per ten seconds of waiting, capped at Blocking, with oldest work winning
|
||||
ties. Completion processing uses promoted priorities. Acquiring a pending asset
|
||||
shares its request rather than submitting another build.
|
||||
|
||||
`EnsurePieceReady()` / `EnsurePiecesReady()` wait for the supplied identities,
|
||||
not the entire library, and return false for failed or cancelled requirements.
|
||||
They do not expand model containers themselves. `EnsureSynthMeshesReady()` waits
|
||||
for the specified instances; `EnsureTextureReady()` waits through upload.
|
||||
`lcModel::WaitForAssets()` combines recursive part and synth waits and updates
|
||||
bounds, but deliberately ignores their failure results. `EnsureAssetsReady()`
|
||||
adds strict failure reporting. `WaitForLoadQueue()` drains all pending work and
|
||||
workers, including unrelated assets; it is not a scoped success check.
|
||||
|
||||
### Choosing an API
|
||||
|
||||
| Need | API and lifetime contract |
|
||||
| --- | --- |
|
||||
| Resolve a name | `FindPiece()` with the intended project and explicit `CreateMissing` / `SearchProjectFolder` arguments. The returned pointer is borrowed; retain it before keeping it beyond its owner's lifetime. |
|
||||
| Retain and start ordinary asynchronous work | `LoadPieceInfo()` without `lcPieceLoadFlag::Wait`; add `Visible` for model/view work. Pair every acquisition with `ReleasePieceInfo()`, including failures. |
|
||||
| Retain without starting work | `AddPieceReference()`, paired with `ReleasePieceInfo()`. This does not make geometry ready. |
|
||||
| Wait for an explicit part set | `EnsurePiecesReady()` (or `EnsurePieceReady()`), check the boolean for strict consumers. Temporary holds end before return, so the caller still needs ownership if it will use the assets afterward. |
|
||||
| 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 | `QueueSynthMesh()` / `EnsureSynthMeshesReady()`. 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; `EnsureTextureReady()` waits through GPU upload. Retain the texture during a direct wait and pair acquisition with `ReleaseTexture()`. |
|
||||
| Synchronize an entire library operation | `WaitForLoadQueue()`; reserve this for operations that truly need all work drained, such as settings changes. Ordinary previews must use scoped waits. |
|
||||
|
||||
Scoped waits may process unrelated completions while pumping publication, but
|
||||
their completion condition depends only on the requested assets. They cannot
|
||||
preempt a worker already parsing, converting, or decoding another asset. Waiting
|
||||
on a single container `PieceInfo` is insufficient for a submodel preview: collect
|
||||
its recursive pieces and per-instance synths, or use the model-level API.
|
||||
|
||||
## Caller policies
|
||||
|
||||
| Caller | Waiting and failure behavior |
|
||||
| --- | --- |
|
||||
| Ordinary LDraw/LDD opening and interactive views | Parse and queue, then return. Draw shared fallback cubes for unavailable geometry; recurse through available children. Update bounds and redraw as assets settle without automatically refitting the camera. |
|
||||
| Inventory import / insertion layout | Wait for required geometry before computing persistent placement; failure prevents using cube bounds as final layout. |
|
||||
| Preview | Wait for its recursive requirements and generated meshes; failure rejects the preview. See [lc_previewwidget.cpp](../common/lc_previewwidget.cpp). |
|
||||
| Thumbnail manager | Acquire required assets asynchronously. Missing library geometry/textures produce an error thumbnail. Pending generated meshes delay rendering; synth failure can use a loaded library mesh. See [lc_thumbnailmanager.cpp](../common/lc_thumbnailmanager.cpp). |
|
||||
| OBJ, COLLADA, 3DS, POV-Ray export | Wait for model requirements, then omit failed meshes and failed generated geometry. Empty output is allowed. Render/write failures still report errors. See [project.cpp](../common/project.cpp). |
|
||||
| Saved images, screenshots, HTML, instructions, printing | Wait for settlement, then render available geometry without loading cubes. HTML parts lists retain missing entries and quantities with blank thumbnails; instruction parts lists omit unavailable thumbnails. Actual rendering/writing failures still fail the operation. |
|
||||
| Model save, CSV, BrickLink inventory | Save references or inventory data without requiring successful geometry loading. |
|
||||
|
||||
Automatic output fitting uses `lcGeometryBoundsMode::AvailableGeometryOnly`;
|
||||
interactive fitting uses `IncludeFallbackGeometry`. A whole-model wait can
|
||||
conservatively include later steps even when output requests an earlier step.
|
||||
Scene flags preventing fallback cubes do not themselves initiate a wait or
|
||||
guarantee success. See [lc_view.cpp](../common/lc_view.cpp) and
|
||||
[lc_scene.h](../common/lc_scene.h).
|
||||
|
||||
`SetRequireCompleteAssets(true)` suppresses unavailable fallback geometry and
|
||||
marks missing assets in the scene; `SetRequireGeneratedMeshes(true)` also excludes
|
||||
the fixed library mesh when the current synth generation is unavailable. A
|
||||
strict renderer checks `HasMissingAssets()` after drawing. Permissive output
|
||||
uses these exclusion rules without making missing assets an operation-wide error.
|
||||
Do not infer readiness from a successfully allocated framebuffer or a nonempty
|
||||
image: it may still contain only the available portion of a model.
|
||||
|
||||
Thumbnail identity is based on `PieceInfo`, not just its filename. Pending
|
||||
thumbnails retain recursive requirements and receive settlement notifications.
|
||||
Category resets release thumbnail IDs before repopulating the list. The submodel
|
||||
category excludes models that include the active model, preventing recursive
|
||||
insertion. `lcThumbnailManager::RefreshPieces()` refreshes affected identities
|
||||
and requirements after Save As remapping; preserve that refresh when changing
|
||||
sources without changing pointers. The preview dock's `RefreshModel()` checks
|
||||
its recursive requirements and respects its lock; it refreshes a displayed
|
||||
submodel affected by editing elsewhere, not an editable temporary preview model.
|
||||
|
||||
Wait and propagate final bounds **before** automatic output fitting, not only
|
||||
inside the later image-rendering call. CLI automatic fitting uses the requested
|
||||
step for one image and the last step for an image range, reusing that camera
|
||||
across the range. Named cameras and explicit positions are preserved. Headless
|
||||
HTML output likewise fits the last step and reuses its camera. Temporary steps,
|
||||
cameras, framebuffer state, and the caller's GL context must be restored on
|
||||
success and failure; texture upload preserves the previous context/surface.
|
||||
|
||||
### Geometry collection versus inventory
|
||||
|
||||
`Project::GetModelParts()` is the settled, flattened geometry list used by 3D
|
||||
exporters. Entries carry an explicit mesh for generated or directly embedded
|
||||
geometry, including root geometry; exporters must prefer that mesh over the
|
||||
shared `PieceInfo` mesh. Failed entries and unfinished/failed synth replacements
|
||||
are filtered out. A retained editing mesh is not exported as the new generation.
|
||||
This list is separate from `GetPartsList()` inventory traversal: changing export
|
||||
collection must not remove missing references from inventories or turn direct
|
||||
model geometry into an ordinary catalog part.
|
||||
|
||||
Project-local filenames are not necessarily safe export identifiers. COLLADA
|
||||
and POV-Ray identifiers use `Project::MakeExportNameFragment()` and
|
||||
`MakeUniqueExportIdentifier()`: preserve ASCII letters/digits, collapse invalid
|
||||
runs into separators, handle empty/long names, and disambiguate collisions.
|
||||
Other formats have their own naming rules. Keep these transformations at the
|
||||
output boundary; do not sanitize lookup names, LDraw references, or stored IDs
|
||||
to satisfy an exporter. HTML output names likewise must not become unchecked
|
||||
paths derived from arbitrary submodel filenames.
|
||||
|
||||
## Project replacement, settings, and shutdown
|
||||
|
||||
Opening a candidate retains the current project until parsing succeeds. A failed
|
||||
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.
|
||||
|
||||
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.
|
||||
Published ordinary meshes are rebuilt selectively using `HasStyleStud`, including
|
||||
flags propagated from nested includes; a style change does not indiscriminately
|
||||
invalidate every library mesh. Pending snapshots must still be refreshed so
|
||||
obsolete settings cannot publish later.
|
||||
Library reload and destruction call `CancelAndDrain()` before destroying sources,
|
||||
registries, or GL resources. It advances the generation, cancels pending work,
|
||||
joins workers, discards obsolete results, and releases notification holds without
|
||||
requiring successful upload or delivering public settlement signals.
|
||||
|
||||
## Performance rationale and verification
|
||||
|
||||
The shared-buffer decision has measured support, not just architectural
|
||||
preference. September 2026 macOS Debug measurements found a first cold cube
|
||||
frame at 219 ms and a first real-mesh frame at 846 ms for a 1,689-reference
|
||||
fighter model; its cold repacks took 6–7 ms. On an Apple M4 renderer, a larger
|
||||
6,696-instance scene at 1280×720 had synchronized median draw times of 75.59 ms
|
||||
uncached versus 35.55 ms with shared buffers, with a 4.43 ms repack.
|
||||
These are historical local measurements, not portable performance guarantees.
|
||||
GPU timer queries, logical GL texture storage, and process RSS do not establish
|
||||
exact physical driver allocation or residency.
|
||||
|
||||
Reconsider buffer ownership only with evidence of repeated repacks exceeding a
|
||||
frame budget, sustained uncached-draw stalls, or substantially greater memory
|
||||
pressure. Cross-platform/GPU profiling and longer soak runs are optional
|
||||
follow-ups, not unfinished streaming implementation stages.
|
||||
|
||||
For changes that affect these contracts, use controlled delays and failure
|
||||
injection with real library/loader/model APIs. Useful acceptance scenarios are:
|
||||
|
||||
| Area | What to verify |
|
||||
| --- | --- |
|
||||
| Cold opening and interaction | Quick cube frame, incremental textured replacement, stable selection/picking, ancestor bounds, and no automatic interactive camera changes. |
|
||||
| Scoped waits and priorities | Occupy the workers, then request an unrelated preview; its dependencies progress ahead of queued ordinary work. Shared requests build/decode once; aging permits background progress. |
|
||||
| Cache and dependency failures | Cold/warm/corrupt-cache equivalence; missing nested includes and missing/corrupt PNGs fail explicitly. Same-name project textures stay isolated. |
|
||||
| Upload and context state | Hidden preview or CLI image completes before a normal draw/event loop. Exercise upload retries and alternate contexts/surfaces/framebuffers. |
|
||||
| Ownership and replacement | Same-name active/candidate/preview models, failed opens, shared assets, last-reference release, external-file undo, and no stale success callbacks after deletion. |
|
||||
| Synth lifecycle | Initial loading cube, repeated pending edits, retained old mesh, cancellation/deletion, nested thumbnails, and final geometry matching the latest control points. |
|
||||
| Output policy | Strict preview failure, permissive geometry/image output, empty scenes, missing parts-list thumbnails, explicit cameras, and restored steps/state after render/write errors. |
|
||||
| Reload and shutdown | Stud changes during loading, library reload, exit with jobs pending, and no-renderer drains. Test both shared-buffer and VBO-disabled rendering where relevant. |
|
||||
|
||||
Historical local Debug and application AddressSanitizer runs exercised these
|
||||
areas, including 120 lifecycle cycles and 2,400 control-point edits. System/Qt
|
||||
frameworks were not instrumented and leak detection was disabled. Those runs
|
||||
are evidence for the tested implementation, not current proof or a replacement
|
||||
for checking new paths; temporary harnesses are not a maintained repository test
|
||||
suite.
|
||||
|
||||
## Invariants and maintenance checks
|
||||
|
||||
- Keep workers on copied source/settings/control-point data and private results.
|
||||
Published meshes, texture pointers, global color registration, GL upload,
|
||||
model mutations, and consumer notifications belong on the UI thread.
|
||||
- Protect queue decisions with the loader mutex; release it before publication,
|
||||
GL upload, or settlement callbacks. Condition waits release the queue mutex
|
||||
while sleeping. Do not hold library locks needed by workers while draining.
|
||||
- Preserve in-flight holds through stale-result disposal and deferred notification.
|
||||
Validate request identity/generation before publishing; deleting an owner must
|
||||
not let a worker or callback access that deleted model/piece.
|
||||
- Preserve unresolved dependency names and diagnostics through caches. A failed
|
||||
include/texture must never be treated as successful partial asset loading.
|
||||
- Distinguish container state, library mesh readiness, and per-instance synth
|
||||
readiness. Choose a caller's failure policy explicitly; settling and succeeding
|
||||
are separate decisions.
|
||||
- When changing this code, exercise shared mesh/texture promotion, last-owner
|
||||
cancellation, failed and successful project replacement, pending synth edits,
|
||||
missing includes/PNGs, cache rebuilds, context restoration, and reload/shutdown.
|
||||
Cover both strict previews and permissive outputs; previous validation is not
|
||||
proof that a new lifecycle path is safe.
|
||||
Reference in new issue
Block a user