Pyrite64 Asset Management and Automatic Memory Cleanup: Internal Strategies Explained

Pyrite64 uses a centralized AssetManager class with UUID-based indexing, std::shared_ptr reference counting, and live file-system polling to handle textures, models, and scripts while automatically reclaiming memory when assets are removed or reloaded.

The AssetManager in the Pyrite64 engine (found in HailToDodongo/pyrite64) implements a self-healing asset pipeline that eliminates manual memory management. By combining eager cataloging with lazy loading and reference-counted ownership, the system ensures GPU and heap resources are released immediately when no longer needed.

Unified Asset Cataloging and UUID Indexing

At startup, AssetManager::reload() in src/project/assetManager.cpp recursively scans the assets folder to build a comprehensive catalog. Every file receives an AssetManagerEntry containing metadata and a unique identifier, categorized by FileType into a std::array<std::vector<AssetManagerEntry>> structure.

The system maintains two critical data structures for O(1) lookups:

  • entries: A type-segregated array of vectors storing the actual asset entries
  • entriesMap: An unordered map providing UUID-to-index translation for instant retrieval
for (const auto &entry : fs::recursive_directory_iterator{assetPath}) {
    if (entry.is_regular_file()) {
        auto path = entry.path();
        watchFiles[path.string()] = Utils::FS::getFileAge(path);
        AssetManagerEntry assetEntry{};
        if (!buildAssetEntry(project, path, assetEntry)) continue;
        
        if (assetEntry.type == FileType::IMAGE && ctx.window) {
            reloadEntry(assetEntry, path.string());
        }
        if (assetEntry.type == FileType::PREFAB) {
            reloadEntry(assetEntry, path.string());
            if (assetEntry.prefab) assetEntry.conf.uuid = assetEntry.prefab->uuid.value;
        }
        entries[(int)assetEntry.type].push_back(assetEntry);
    }
}

Implementation reference: src/project/assetManager.cpp lines 80-104

Reference-Counted Resource Ownership

Heavy resources including GPU textures, 3D meshes, and prefabs are wrapped in std::shared_ptr to enable automatic memory cleanup. The reloadEntry() method in src/project/assetManager.cpp is the sole location where GPU-bound objects are instantiated, ensuring centralized ownership tracking.

case FileType::IMAGE:
    entry.texture = std::make_shared<Renderer::Texture>(ctx.gpu, path, isMono);
    break;
case FileType::PREFAB:
    entry.prefab = std::make_shared<Prefab>();
    entry.prefab->deserialize(Utils::FS::loadTextFile(path));
    break;

When reload() clears the type vectors using for (auto &e : entries) e.clear();, all shared_ptr references go out of scope. Their destructors automatically invoke the corresponding GPU memory or heap deallocators without explicit delete calls.

Live File-System Watching and Incremental Reload

The pollWatch() method implements a differential file-system watcher that executes every 2 seconds (configurable via kMinInterval). It snapshots file timestamps, compares them against the previous state stored in watchFiles, and categorizes changes into added, modified, or removed assets.

if (it == watchFiles.end()) {
    addedAssets.push_back(pathStr);
} else if (it->second != age) {
    modifiedAssets.push_back(pathStr);
}

Implementation reference: src/project/assetManager.cpp lines 71-87

When paths are detected as removed, entries are erased from both the entries vectors and entriesMap. Because these containers store shared_ptr objects, erasure immediately drops the final reference count, triggering destructor-based cleanup of the underlying resources. Modified assets trigger reloadEntry() to refresh the resource while maintaining the same UUID mapping.

Practical Implementation Examples

Querying Assets by UUID

Runtime code retrieves assets safely without manual memory management concerns using getEntryByUUID():

uint64_t uuid = 0x12345678abcdef00;
auto *entry = ctx.project->getAssets().getEntryByUUID(uuid);
if (entry && entry->type == Project::FileType::IMAGE) {
    auto tex = entry->texture;  // shared_ptr copy increments ref-count
    // Texture remains valid for this scope; automatic release follows
}

Reference: src/project/assetManager.h lines 33-39

Forcing Full Asset Reloads

For bulk file-system changes or manual refresh triggers, the reload cycle can be invoked programmatically:

if (ctx.project->getAssets().pollWatch()) {
    ctx.project->getAssets().reload();  // Clears vectors, frees shared_ptrs
    // Assets re-scanned and re-loaded automatically
}

Reference: src/main.cpp line 274

Runtime Script Creation

New assets integrate seamlessly into the watching system upon creation:

bool created = ctx.project->getAssets().createScript("MyNewScript", "scripts");
if (created) {
    // File appears on disk; next pollWatch() cycle picks it up automatically
}

Reference: src/project/assetManager.h lines 53-55

Fallback Texture Handling

The manager provides a lazily-initialized fallback texture for failed loads, itself managed via shared_ptr:

const std::shared_ptr<Renderer::Texture> &Project::AssetManager::getFallbackTexture() {
    if (!fallbackTex) fallbackTex = std::make_shared<Renderer::Texture>(/* ... */);
    return fallbackTex;
}

Location: src/project/assetManager.h lines 49-51

Summary

  • Unified indexing: Assets are cataloged at startup into type-specific vectors with UUID mapping for constant-time lookups
  • Automatic memory management: GPU textures, meshes, and prefabs use std::shared_ptr ownership; clearing entries or erasing vectors triggers immediate destructor-based cleanup
  • Live watching: pollWatch() scans every 2 seconds for file changes, handling incremental updates and removals without manual intervention
  • Safe access patterns: The getEntryByUUID() API returns pointers to entries containing shared pointers, ensuring resources remain valid during access
  • Zero manual deletion: No explicit delete calls are required anywhere in the asset pipeline

Frequently Asked Questions

How does Pyrite64 handle memory cleanup when assets are deleted from disk?

When pollWatch() detects a removed file, it erases the corresponding entry from the entries vector and entriesMap. Because these containers store std::shared_ptr objects, erasure destroys the last reference to the resource, automatically invoking the destructor to free GPU memory or heap allocations immediately.

What is the file system polling interval in Pyrite64's AssetManager?

The pollWatch() method executes every 2 seconds by default, defined by the kMinInterval constant. This interval balances responsiveness to asset changes with minimal runtime overhead during editor sessions.

How are GPU textures managed to prevent memory leaks?

GPU textures are instantiated exclusively within reloadEntry() as std::shared_ptr<Renderer::Texture> and stored in AssetManagerEntry structures. When entries are cleared during a full reload() or individually erased due to file deletion, the shared pointer reference count drops to zero, automatically destroying the Renderer::Texture object and its associated GPU resources.

Can assets be queried efficiently at runtime without string path lookups?

Yes. The AssetManager maintains an entriesMap that maps 64-bit UUIDs to type-index pairs, enabling O(1) retrieval via getEntryByUUID(). This avoids expensive string comparisons or recursive directory searches during gameplay or editor operations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →