Async USDZ Loading System - Usage Guide
Overview
The async loading system allows you to load USDZ files with many models without blocking the main thread. The engine remains responsive during loading, and you can track progress for UI feedback.
Features
✅ Non-blocking loading - Engine continues running while assets load
✅ Progress tracking - Monitor loading progress per entity or globally
✅ Fallback meshes - Automatically loads a cube if loading fails
✅ Backward compatible - Old synchronous APIs still work
Basic Usage
Load Entity Mesh Asynchronously
// Create entity
let entityId = createEntity()
// Load mesh asynchronously (fire and forget)
setEntityMeshAsync(
entityId: entityId,
filename: "large_model",
withExtension: "usdz"
)
// Engine continues running - mesh appears when loaded
With Completion Callback
let entityId = createEntity()
setEntityMeshAsync(
entityId: entityId,
filename: "large_model",
withExtension: "usdz"
) { success in
if success {
print("✅ Model loaded successfully!")
} else {
print("❌ Failed to load - fallback cube used")
}
}
Load Entire Scene Asynchronously
loadSceneAsync(
filename: "complex_scene",
withExtension: "usdz"
) { success in
if success {
print("Scene loaded with all entities")
}
}
Progress Tracking
Check if Assets are Loading
Task {
let isLoading = await AssetLoadingState.shared.isLoadingAny()
if isLoading {
print("Assets are currently loading...")
}
}
Get Loading Count
Task {
let count = await AssetLoadingState.shared.loadingCount()
print("Loading \(count) entities")
}
Get Detailed Progress
Task {
let (current, total) = await AssetLoadingState.shared.totalProgress()
let percentage = Float(current) / Float(total) * 100
print("Progress: \(percentage)% (\(current)/\(total) meshes)")
}
Get Progress Summary String
Task {
let summary = await AssetLoadingState.shared.loadingSummary()
print(summary) // "Loading large_model.usdz: 5/20 meshes"
}
Track Specific Entity
Task {
if let progress = await AssetLoadingState.shared.getProgress(for: entityId) {
print("\(progress.filename): \(progress.currentMesh)/\(progress.totalMeshes)")
print("Percentage: \(progress.percentage * 100)%")
}
}
Untold Editor Integration
Display Loading UI
// In your editor's update loop or UI
Task {
if await AssetLoadingState.shared.isLoadingAny() {
let summary = await AssetLoadingState.shared.loadingSummary()
// Show loading indicator with summary text
showLoadingIndicator(text: summary)
} else {
// Hide loading indicator
hideLoadingIndicator()
}
}
Progress Bar Example
Task {
let allProgress = await AssetLoadingState.shared.getAllProgress()
for progress in allProgress {
let percentage = progress.percentage * 100
print("📦 \(progress.filename): \(Int(percentage))%")
// Update UI progress bar
updateProgressBar(
id: progress.entityId,
filename: progress.filename,
percentage: percentage
)
}
}
Error Handling
Automatic Fallback
When async loading fails, a fallback cube mesh is automatically loaded:
setEntityMeshAsync(
entityId: entityId,
filename: "missing_file",
withExtension: "usdz"
) { success in
if !success {
// Entity now has a cube mesh as fallback
print("Loaded fallback cube")
}
}
Manual Error Handling
setEntityMeshAsync(
entityId: entityId,
filename: "model",
withExtension: "usdz"
) { success in
if !success {
// Handle error - entity has fallback cube
showErrorDialog("Failed to load model.usdz")
// Optionally destroy entity or retry
// destroyEntity(entityId: entityId)
}
}
Backward Compatibility
The old synchronous APIs still work unchanged:
// Old way (blocks main thread)
setEntityMesh(
entityId: entityId,
filename: "model",
withExtension: "usdz"
)
// New way (non-blocking)
setEntityMeshAsync(
entityId: entityId,
filename: "model",
withExtension: "usdz"
)
Advanced Usage
Loading Multiple Models
let entity1 = createEntity()
let entity2 = createEntity()
let entity3 = createEntity()
// All three load in parallel without blocking
setEntityMeshAsync(entityId: entity1, filename: "model1", withExtension: "usdz")
setEntityMeshAsync(entityId: entity2, filename: "model2", withExtension: "usdz")
setEntityMeshAsync(entityId: entity3, filename: "model3", withExtension: "usdz")
// Track total progress
Task {
while await AssetLoadingState.shared.isLoadingAny() {
let summary = await AssetLoadingState.shared.loadingSummary()
print(summary)
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
}
print("All models loaded!")
}
Coordinate System Conversion
setEntityMeshAsync(
entityId: entityId,
filename: "blender_model",
withExtension: "usdz",
coordinateConversion: .forceZUpToYUp
)
Implementation Details
What Runs on Background Thread
- Reading USDZ file from disk
- Parsing MDL data structures
- Loading texture data
- Applying coordinate transformations
What Runs on Main Thread
- Creating Metal resources (MTKMesh, MTLTexture)
- Registering ECS components
- Updating entity transforms
- Creating fallback meshes on error
Thread Safety
All AssetLoadingState operations are thread-safe via Swift's actor model. You can safely query loading state from any thread.
Performance Tips
- Prefer async for large files (>20 models or >50MB)
- Use sync for small files (1-5 models, <10MB) - less overhead
- Batch loads together - loading multiple files in parallel is efficient
- Monitor progress - use for UI feedback during long loads
Migration Guide
Before (Blocking)
func loadGameAssets() {
let entity = createEntity()
setEntityMesh(entityId: entity, filename: "huge_scene", withExtension: "usdz")
// Engine was frozen here ❌
}
After (Non-blocking)
func loadGameAssets() {
let entity = createEntity()
setEntityMeshAsync(entityId: entity, filename: "huge_scene", withExtension: "usdz") {
success in
if success {
print("Scene ready!")
}
}
// Engine continues running ✅
}