- Added documentation for Overte authentication implementation in `docs/OVERTE_AUTH.md`. - Introduced new GLB files for cube and sphere primitives in `examples/primitives/`. - Created a JSON file `examples/test_entities.json` containing sample entities for testing. - Added a build and test script `scripts/build_and_test.sh` for streamlined building and verification of the project. - Implemented a CI test runner script `scripts/ci-test.sh` to automate testing processes. - Created a script `scripts/run_with_auth.sh` to facilitate running the Starworld client with Overte authentication.
10 KiB
ModelCache Implementation - Technical Documentation
Overview
The ModelCache is a C++ singleton class that handles HTTP(S) asset downloading for 3D models in Starworld. It follows Overte's ResourceCache architecture but is implemented in pure C++ using libcurl instead of Qt's networking stack.
Location:
- Header:
src/ModelCache.hpp - Implementation:
src/ModelCache.cpp
Purpose:
- Download 3D models from HTTP/HTTPS URLs
- Cache downloaded models to avoid re-downloading
- Provide async callbacks for download progress and completion
- Support model loading via StardustXR's Model::direct(PathBuf)
Architecture Comparison: Overte vs Starworld
Overte's ResourceCache Pattern
Overte uses a sophisticated caching system in libraries/networking/src/:
ResourceCache (base class)
├── ResourceRequest (abstract)
│ ├── HTTPResourceRequest
│ ├── AssetResourceRequest (ATP protocol)
│ └── FileResourceRequest
├── ModelCache (extends ResourceCache)
├── TextureCache (extends ResourceCache)
└── AnimationCache (extends ResourceCache)
Key Features:
- Qt-based networking (QNetworkAccessManager, QNetworkReply)
- Resource lifecycle management (loading, loaded, cached, unused)
- Request queueing and priority system
- LRU cache eviction
- ATP protocol support (atp:// URLs)
- Automatic retry with exponential backoff
Starworld's ModelCache
Our implementation is simplified but compatible:
ModelCache (singleton)
├── Uses libcurl for HTTP downloads
├── SHA256-based filename hashing
├── Async callbacks (completion, progress)
├── Thread-safe resource tracking
└── Simple state machine (NotStarted → Downloading → Completed/Failed)
Design Decisions:
- No Qt dependency: Uses libcurl (more lightweight, cross-platform)
- C++ only: Integrates directly into Starworld's C++ codebase
- Async via std::thread: Simple background download threads
- SHA256 hashing: URL → unique filename for cache storage
- StardustXR integration: Returns local paths for Model::direct()
Implementation Details
Cache Directory Structure
~/.cache/starworld/
├── primitives/ # Blender-generated test models
│ ├── cube.glb # Red cube (Box entities)
│ ├── sphere.glb # Green sphere (Sphere entities)
│ └── model.glb # Blue icosphere (Model placeholder)
└── models/ # Downloaded HTTP models
├── <sha256-hash-1>.glb
├── <sha256-hash-2>.gltf
└── <sha256-hash-N>.fbx
Filename Generation:
std::string sha256(const std::string& url); // SHA256 hash
std::string getExtensionFromUrl(const std::string& url); // .glb, .gltf, .fbx, .obj
std::string filename = sha256(url) + getExtensionFromUrl(url);
API Usage
// Get singleton instance
ModelCache& cache = ModelCache::instance();
// Request a model (async)
cache.requestModel(
"https://example.com/models/chair.glb",
// Completion callback
[](const std::string& url, bool success, const std::string& localPath) {
if (success) {
std::cout << "Model ready: " << localPath << std::endl;
// Pass localPath to Model::direct() in Rust bridge
} else {
std::cerr << "Download failed: " << url << std::endl;
}
},
// Progress callback (optional)
[](const std::string& url, size_t bytesReceived, size_t bytesTotal) {
float percent = (bytesReceived * 100.0f) / bytesTotal;
std::cout << "Downloading: " << percent << "%" << std::endl;
}
);
// Synchronous checks
if (cache.isCached(url)) {
std::string path = cache.getCachedPath(url);
}
ModelCache::State state = cache.getState(url);
Integration with StardustBridge
Before (direct pass-through):
bool StardustBridge::setNodeModel(NodeId id, const std::string& modelUrl) {
if (m_fnSetModel) {
return m_fnSetModel(id, modelUrl.c_str()) == 0;
}
return true;
}
After (with ModelCache):
bool StardustBridge::setNodeModel(NodeId id, const std::string& modelUrl) {
// Check if URL is HTTP(S)
if (modelUrl.substr(0, 7) == "http://" || modelUrl.substr(0, 8) == "https://") {
// Download via ModelCache, then pass local path to bridge
ModelCache::instance().requestModel(
modelUrl,
[this, id](const std::string& url, bool success, const std::string& localPath) {
if (success && m_fnSetModel) {
m_fnSetModel(id, localPath.c_str());
}
}
);
return true; // Download initiated
}
// Direct URL (file://, atp://, etc.)
if (m_fnSetModel) {
return m_fnSetModel(id, modelUrl.c_str()) == 0;
}
return true;
}
Thread Safety
The ModelCache uses std::mutex to protect shared state:
mutable std::mutex mutex_;
std::unordered_map<std::string, std::shared_ptr<ModelResource>> resources_;
std::unordered_map<std::string, std::vector<CompletionCallback>> completionCallbacks_;
std::unordered_map<std::string, std::vector<ProgressCallback>> progressCallbacks_;
All public methods acquire the mutex before accessing maps. Downloads run in detached std::thread instances.
Error Handling
Download failures:
- Network errors (CURLE_* codes)
- HTTP errors (4xx, 5xx)
- File system errors (can't create cache dir, can't write file)
Fallback behavior:
- On download failure, completion callback receives
success=false - StardustBridge logs error but doesn't crash
- Rust bridge falls back to primitive models via
get_model_path()
Differences from Overte
| Feature | Overte ResourceCache | Starworld ModelCache |
|---|---|---|
| Networking | Qt (QNetworkAccessManager) | libcurl |
| Threading | Qt event loop | std::thread |
| Caching | LRU with size limits | Simple hash-based (no eviction) |
| Retry logic | Exponential backoff | None (TODO) |
| Progress | QNetworkReply signals | CURL progress callback |
| ATP support | Full AssetClient | Not yet implemented |
| Request queue | Priority-based queue | No queue (immediate download) |
| Cache eviction | LRU with max size | None (grows indefinitely) |
Future Enhancements
1. ATP Protocol Support
Overte's asset server uses atp:// URLs. To support them:
// Map atp:// to http:// using domain asset server info
std::string resolveATPUrl(const std::string& atpUrl, const std::string& assetServerHost) {
// atp://hash.modelType → http://assetserver:port/hash.modelType
// Requires AssetClient integration or manual URL construction
}
2. Request Queueing
Limit concurrent downloads (like Overte's request limit):
class ModelCache {
static constexpr size_t MAX_CONCURRENT = 10;
std::queue<std::string> pendingDownloads_;
std::atomic<size_t> activeDownloads_{0};
};
3. Cache Eviction (LRU)
Track last access time and enforce max cache size:
struct ModelResource {
std::chrono::system_clock::time_point lastAccessed;
size_t fileSize;
};
void ModelCache::evictLRU(size_t targetSize);
4. Retry Logic
Implement exponential backoff like Overte:
struct ModelResource {
int attempts = 0;
static constexpr int MAX_ATTEMPTS = 3;
};
void ModelCache::retryDownload(const std::string& url, int delay_ms);
5. Content-Type Detection
Use HTTP headers instead of URL heuristics:
static size_t headerCallback(char* buffer, size_t size, size_t nitems, void* userdata) {
std::string header(buffer, size * nitems);
if (header.find("Content-Type: model/gltf-binary") != std::string::npos) {
// Use .glb extension
}
}
Testing
Unit Tests (TODO)
TEST(ModelCache, DownloadHTTP) {
ModelCache& cache = ModelCache::instance();
bool completed = false;
cache.requestModel(
"https://example.com/test.glb",
[&](const std::string& url, bool success, const std::string& path) {
EXPECT_TRUE(success);
EXPECT_TRUE(std::filesystem::exists(path));
completed = true;
}
);
// Wait for async completion
while (!completed) { std::this_thread::sleep_for(100ms); }
}
Integration Testing
- Real Overte server: Connect to domain with Model entities
- Verify downloads: Check
~/.cache/starworld/models/for cached files - Check rendering: Confirm models load via Model::direct() in Rust bridge
- Network failure: Test with unreachable URLs, verify error handling
Performance Considerations
Memory
- Each ModelResource is ~1KB (metadata only, not file contents)
- No memory cache (files stay on disk)
- Completed callbacks are cleared after invocation
Disk I/O
- Downloads write directly to disk (streaming, not buffered in RAM)
- No compression (stores as-downloaded)
- No cache size limit (manual cleanup via
clearCache())
Network
- Parallel downloads (no limit, each in its own thread)
- No request batching
- No connection pooling (libcurl creates new connection per request)
Optimization TODO:
- Use curl_multi for connection pooling
- Limit concurrent downloads
- Implement cache size monitoring
Build Requirements
CMakeLists.txt additions:
find_package(CURL REQUIRED)
find_package(OpenSSL REQUIRED)
add_executable(stardust-overte-client
...
src/ModelCache.cpp
)
target_link_libraries(stardust-overte-client PRIVATE
CURL::libcurl
OpenSSL::Crypto
)
System dependencies:
# Debian/Ubuntu
sudo apt install libcurl4-openssl-dev libssl-dev
# Fedora
sudo dnf install libcurl-devel openssl-devel
# Arch
sudo pacman -S curl openssl
References
- Overte ResourceCache:
overte/libraries/networking/src/ResourceCache.{h,cpp} - Overte ModelCache:
overte/libraries/model-networking/src/model-networking/ModelCache.{h,cpp} - Overte HTTPResourceRequest:
overte/libraries/networking/src/HTTPResourceRequest.cpp - libcurl documentation: https://curl.se/libcurl/c/
- OpenSSL SHA256: https://www.openssl.org/docs/man1.1.1/man3/SHA256.html
Summary
The ModelCache successfully implements HTTP asset downloading for Starworld, following Overte's architectural patterns while using modern C++ and standard libraries. It provides async model loading with caching, integrating seamlessly with the StardustXR bridge via local file paths.
Key Achievement: Phase 2 of the roadmap (Asset Pipeline) is now complete! ✅