How to safely unload meshes and textures?

If you are a new Irrlicht Engine user, and have a newbie-question, this is the forum for you. You may also post general programming questions here.
Post Reply
dart_theg
Posts: 53
Joined: Sun Dec 29, 2024 3:13 am

How to safely unload meshes and textures?

Post by dart_theg »

I am using irrlicht for my game engine and when I remove meshes from the cache, same as textures, to free up memory, the next frame the scene manager crashes because I’m assuming it tries to find the memory for these assets and it finds something unrelated. How do I safely unload assets?
CuteAlien
Admin
Posts: 9926
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: How to safely unload meshes and textures?

Post by CuteAlien »

Your assumption why it crashes is probably wrong. You can for example add the following line in example 09 in loadModel after creating the models:
Device->getSceneManager()->getMeshCache()->removeMesh(mesh);
You'll see it still works. Only difference is that it'll load the mesh again next time you open it.

A bit more tricky tend to be removing nodes. Which you either should do outside the render-loop or you have to use ISceneManager::addToDeletionQueue to do that safely.

So... first step is to use a debugger and check where it crashes. Hard to make any guesses without that. Thought if I had to guess - probably you mess up reference counting somewhere. See https://irrlicht.sourceforge.io/docu/cl ... unted.html for when you are allowed to drop or delete meshes. If that happens once too often then you mess up the reference counter and you can get crashes.
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
dart_theg
Posts: 53
Joined: Sun Dec 29, 2024 3:13 am

Re: How to safely unload meshes and textures?

Post by dart_theg »

I just did the whole remove via getMeshCache, checked that the reference counts are 1, and when I run my unload my program crashes and points to a completely unrelated part of my project. (and if I comment out the line it claims is causing it, it'll say it's the next line, but these are 100% unrelated) I do this all prior to the render loop...
dart_theg
Posts: 53
Joined: Sun Dec 29, 2024 3:13 am

Re: How to safely unload meshes and textures?

Post by dart_theg »

Ok, it seems I've done it but my memory usage is still the same, even though the mesh cache size is 0. Any idea?
n00bc0de
Posts: 116
Joined: Tue Oct 04, 2022 1:21 am

Re: How to safely unload meshes and textures?

Post by n00bc0de »

The mesh could be referenced somewhere else. Try checking the reference count before removing it.

Something like this:

Code: Select all

if(mesh->getReferenceCount() > 0)
{
    SceneManager->getMeshCache()->removeMesh(mesh);
}
n00bc0de
Posts: 116
Joined: Tue Oct 04, 2022 1:21 am

Re: How to safely unload meshes and textures?

Post by n00bc0de »

dart_theg wrote: Sat Oct 25, 2025 6:37 pm Ok, it seems I've done it but my memory usage is still the same, even though the mesh cache size is 0. Any idea?
You should look at CMeshCache.cpp to get a good idea of what it is doing. If the mesh is in the mesh cache, then it will drop the reference to the array and remove it from the cache.

From there, open IReferenceCounted.cpp (its in the include folder). Look at drop and you see these lines:

Code: Select all

--ReferenceCounter;
if (!ReferenceCounter)
{
	delete this;
	return true;
}
To understand this, you need to understand that everything in irrlicht inherits from IReferenceCounted. So every object has a ReferenceCounter. If that reference counter reaches 0, then drop() will remove it. If it doesn't, then drop() doesn't do anything. If it gets below 0, then your program is likely going to crash.

Basically, removing it from the cache won't automatically set the reference count to 0. Any function that called grab() on the mesh still has an active reference to it until it has been dropped.

However, I am no expert so CuteAlien might have something to correct me on this.
CuteAlien
Admin
Posts: 9926
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: How to safely unload meshes and textures?

Post by CuteAlien »

Nope, nothing to say - just like that ;-)

Other possibility is that you still have a pointer to the mesh somewhere but didn't grab - then the pointer is now pointing to bad memory.
Really hard to find that stuff without debugging.
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
Noiecity
Posts: 314
Joined: Wed Aug 23, 2023 7:22 pm
Contact:

Re: How to safely unload meshes and textures?

Post by Noiecity »

In fact, the memory will not decrease, but it will not increase a second time either (it may allocate a little more memory the second time). If you delete and clear the model from memory, Irrlicht will continue to reuse the allocated memory. I can confirm this in an example on Linux: the memory does not decrease because it is then reused or more memory is allocated if necessary. in other words, the memory space that did not decrease is free to load another model in its place, for example... or so I believe...

In other words, you won't see the memory decrease, but consider it as if it had decreased and is technically free to load another model by reusing the resources.
...In other words, you don't have to worry that memory will grow indefinitely even if you clear the model; free memory will simply remain available to assign values.

Code: Select all

// toggle_sydney_memory_stable.cpp
// C++98 - Memory stability test for Irrlicht SDK
// Tests that memory stabilizes after initial allocations (no leaks)
// SPACE toggles load/unload, ESC quits, 'R' resets memory baseline

#include <irrlicht.h>
#include <cstdio>
#include <vector>
#ifdef _WIN32
  #define WIN32_LEAN_AND_MEAN
  #include <windows.h>
  #include <psapi.h>
#else
  #include <stdio.h>
  #include <unistd.h>
#endif
#ifdef _IRR_WINDOWS_
#pragma comment(lib, "Irrlicht.lib")
#endif*/
using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace gui;

// Cross-platform RSS monitoring
static long getProcessRSS_kB()
{
#ifdef _WIN32
    PROCESS_MEMORY_COUNTERS pmc;
    if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
        return (long)(pmc.WorkingSetSize / 1024);
    }
    return -1;
#else
    FILE* f = fopen("/proc/self/status","r");
    if (!f) return -1;
    char line[256];
    long result = -1;
    while (fgets(line,sizeof(line),f)) {
        if (strncmp(line,"VmRSS:",6)==0) {
            char *p = line;
            while (*p && (*p < '0' || *p > '9')) ++p;
            if (*p) result = atol(p);
            break;
        }
    }
    fclose(f);
    return result;
#endif
}

class SimpleReceiver : public IEventReceiver {
public:
    SimpleReceiver(): tog(false), quit(false), reset_base(false) {}
    virtual bool OnEvent(const SEvent& e) {
        if (e.EventType==EET_KEY_INPUT_EVENT && !e.KeyInput.PressedDown) {
            if (e.KeyInput.Key==KEY_SPACE) { tog = true; return true; }
            if (e.KeyInput.Key==KEY_ESCAPE) { quit = true; return true; }
            if (e.KeyInput.Key==KEY_KEY_R) { reset_base = true; return true; }
        }
        return false;
    }
    bool consumeToggle() { if (tog) { tog=false; return true; } return false; }
    bool consumeReset() { if (reset_base) { reset_base=false; return true; } return false; }
    bool shouldQuit() const { return quit; }
private:
    bool tog, quit, reset_base;
};

// Memory monitoring with baseline
class MemoryMonitor {
private:
    long baseline_rss;
    std::vector<long> history;
    int cycle_count;

public:
    MemoryMonitor() : baseline_rss(-1), cycle_count(0) {}

    void setBaseline() {
        baseline_rss = getProcessRSS_kB();
        history.clear();
        history.push_back(baseline_rss);
        cycle_count = 0;
        std::printf("[MEMORY] Baseline set: %ld kB\n", baseline_rss);
    }

    void record(const char* context) {
        long current_rss = getProcessRSS_kB();
        history.push_back(current_rss);
        cycle_count++;

        std::printf("[MEMORY %d] %s - RSS: %ld kB", cycle_count, context, current_rss);

        if (baseline_rss != -1) {
            long diff = current_rss - baseline_rss;
            std::printf(" (delta from baseline: %+ld kB)", diff);
        }

        if (history.size() > 1) {
            long prev_rss = history[history.size()-2];
            long instant_diff = current_rss - prev_rss;
            std::printf(" (instant: %+ld kB)", instant_diff);
        }
        std::printf("\n");
    }

    void analyzeStability() {
        if (history.size() < 4) return;

        // Check if memory has stabilized
        long recent_avg = 0;
        int count = history.size() > 10 ? 10 : history.size();
        for (int i = history.size() - count; i < history.size(); i++) {
            recent_avg += history[i];
        }
        recent_avg /= count;

        long max_deviation = 0;
        for (int i = history.size() - count; i < history.size(); i++) {
            long dev = std::abs(history[i] - recent_avg);
            if (dev > max_deviation) max_deviation = dev;
        }

        std::printf("[STABILITY] Last %d samples: avg=%ld kB, max deviation=%ld kB (%s)\n",
                   count, recent_avg, max_deviation,
                   max_deviation < 1024 ? "STABLE" : "UNSTABLE");
    }
};

// Camera positioning
static void positionCameraForNode(ISceneManager* smgr, ISceneNode* node)
{
    if (!node) {
        smgr->addCameraSceneNode(0, vector3df(0,20,-60), vector3df(0,0,0));
        return;
    }

    core::aabbox3d<f32> box = node->getBoundingBox();
    core::matrix4 absMat = node->getAbsoluteTransformation();
    core::vector3d<f32> corners[8];
    corners[0] = core::vector3d<f32>(box.MinEdge.X, box.MinEdge.Y, box.MinEdge.Z);
    corners[1] = core::vector3d<f32>(box.MaxEdge.X, box.MinEdge.Y, box.MinEdge.Z);
    corners[2] = core::vector3d<f32>(box.MinEdge.X, box.MaxEdge.Y, box.MinEdge.Z);
    corners[3] = core::vector3d<f32>(box.MaxEdge.X, box.MaxEdge.Y, box.MinEdge.Z);
    corners[4] = core::vector3d<f32>(box.MinEdge.X, box.MinEdge.Y, box.MaxEdge.Z);
    corners[5] = core::vector3d<f32>(box.MaxEdge.X, box.MinEdge.Y, box.MaxEdge.Z);
    corners[6] = core::vector3d<f32>(box.MinEdge.X, box.MaxEdge.Y, box.MaxEdge.Z);
    corners[7] = core::vector3d<f32>(box.MaxEdge.X, box.MaxEdge.Y, box.MaxEdge.Z);

    core::aabbox3d<f32> wb;
    bool first=true;
    for (int i=0;i<8;++i) {
        absMat.transformVect(corners[i]);
        if (first) { wb.reset(corners[i]); first=false; }
        else wb.addInternalPoint(corners[i]);
    }

    core::vector3d<f32> center = wb.getCenter();
    core::vector3d<f32> extent = wb.getExtent();
    f32 r = core::max_<f32>(extent.getLength(), 1.0f);
    core::vector3d<f32> camPos = center + core::vector3df(0.0f, r*0.6f, r*2.5f);
    smgr->addCameraSceneNode(0, camPos, center);
}

// Resource loading with cache monitoring
static void loadResources(ISceneManager* smgr, IVideoDriver* driver,
                         const char* modelPath, const char* texPath,
                         IAnimatedMesh*& outMesh, ISceneNode*& outNode, ITexture*& outTex)
{
    outMesh = 0; outNode = 0; outTex = 0;

    IMeshCache* meshCache = smgr->getMeshCache();
    std::printf("[LOAD] Mesh cache size before: %d\n", (int)meshCache->getMeshCount());

    IAnimatedMesh* mesh = smgr->getMesh(modelPath);
    if (!mesh) {
        char alt[512]; std::sprintf(alt, "media/%s", modelPath);
        mesh = smgr->getMesh(alt);
    }
    if (!mesh) {
        std::printf("[LOAD] Failed to load mesh '%s'\n", modelPath);
        return;
    }
    outMesh = mesh;

    IAnimatedMeshSceneNode* node = smgr->addAnimatedMeshSceneNode(mesh);
    if (!node) {
        std::printf("[LOAD] Failed to create mesh node\n");
        return;
    }
    node->setMaterialFlag(EMF_LIGHTING, false);
    s32 frames = mesh->getFrameCount();
    if (frames > 1) node->setFrameLoop(0, frames-1);
    node->setAnimationSpeed(15.0f);
    outNode = node;

    ITexture* tex = driver->getTexture(texPath);
    if (!tex) {
        char alt[512]; std::sprintf(alt, "media/%s", texPath);
        tex = driver->getTexture(alt);
    }
    if (tex) {
        outTex = tex;
        for (u32 i=0;i<node->getMaterialCount();++i) {
            node->setMaterialTexture(i, tex);
        }
    }

    std::printf("[LOAD] Mesh cache size after: %d\n", (int)meshCache->getMeshCount());
    std::printf("[LOAD] Loaded '%s' (%d frames)\n", modelPath, (int)frames);
}

// Cleanup that focuses on reference counting
static void unloadResources(ISceneManager* smgr, IVideoDriver* driver,
                           IAnimatedMesh*& mesh, ISceneNode*& node, ITexture*& tex)
{
    std::printf("[UNLOAD] Starting cleanup\n");
    IMeshCache* meshCache = smgr->getMeshCache();

    if (node) {
        // Report node state before cleanup
        std::printf("[UNLOAD] Node children: %d\n", (int)node->getChildren().size());

        // Clear materials
        for (u32 i = 0; i < node->getMaterialCount(); ++i) {
            node->setMaterialTexture(i, NULL);
        }

        // Remove mesh reference from animated node
        IAnimatedMeshSceneNode* animNode = (IAnimatedMeshSceneNode*)node;
        if (animNode) {
            animNode->setMesh(NULL);
        }

        node->remove();
        node = NULL;
    }

    smgr->clear();

    if (tex) {
        driver->removeTexture(tex);
        tex = NULL;
    }

    if (mesh) {
        int refs_before = mesh->getReferenceCount();
        std::printf("[UNLOAD] Mesh references: %d\n", refs_before);

        meshCache->removeMesh(mesh);
        mesh = NULL;
    }

    // Don't clear entire cache - let Irrlicht manage its pools
    // meshCache->clear();

    std::printf("[UNLOAD] Mesh cache size after cleanup: %d\n", (int)meshCache->getMeshCount());
    std::printf("[UNLOAD] Cleanup completed\n");
}

int main()
{
    IrrlichtDevice* device = createDevice(EDT_OPENGL, dimension2d<u32>(1024,768), 16, false, false, false, 0);
    if (!device) {
        std::printf("Failed to create Irrlicht device\n");
        return 1;
    }

    SimpleReceiver recv;
    device->setEventReceiver(&recv);

    IVideoDriver* driver = device->getVideoDriver();
    ISceneManager* smgr = device->getSceneManager();
    IGUIEnvironment* guienv = device->getGUIEnvironment();
    IGUIFont* font = guienv->getBuiltInFont();

    const char* model = "../../media/sydney.md2";
    const char* texture = "../../media/sydney.bmp";

    IAnimatedMesh* mesh = 0;
    ISceneNode* node = 0;
    ITexture* tex = 0;
    bool loaded = false;
    int cycle = 0;

    MemoryMonitor memMonitor;
    memMonitor.setBaseline();

    std::printf("=== MEMORY STABILITY TEST ===\n");
    std::printf("Initial state - no model loaded\n");
    memMonitor.record("Initial state");

    // Main loop
    while (device->run()) {
        if (recv.shouldQuit()) break;

        if (recv.consumeReset()) {
            std::printf("\n=== RESETTING MEMORY BASELINE ===\n");
            memMonitor.setBaseline();
        }

        if (recv.consumeToggle()) {
            if (loaded) {
                std::printf("\n--- UNLOAD CYCLE %d ---\n", cycle);
                unloadResources(smgr, driver, mesh, node, tex);
                loaded = false;
                memMonitor.record("After unload");
            } else {
                std::printf("\n--- LOAD CYCLE %d ---\n", cycle);
                loadResources(smgr, driver, model, texture, mesh, node, tex);
                if (node) positionCameraForNode(smgr, node);
                loaded = (node != 0);
                memMonitor.record("After load");
                cycle++;

                // Analyze stability every few cycles
                if (cycle % 5 == 0) {
                    memMonitor.analyzeStability();
                }
            }
        }

        // Rendering
        driver->beginScene(true, true, SColor(255,100,101,140));
        smgr->drawAll();
        guienv->drawAll();

        // Display information
        char buf[512];
        long rss = getProcessRSS_kB();

        std::sprintf(buf, "Cycle: %d  Loaded: %s  RSS: %ld kB",
                    cycle, loaded ? "YES" : "NO", rss);
        if (font) font->draw(buf, core::rect<s32>(10,10,900,30), SColor(255,255,255,255));

        std::sprintf(buf, "Mesh: %s  Node: %s  Texture: %s",
                     mesh ? "present" : "null",
                     node ? "present" : "null",
                     tex ? "present" : "null");
        if (font) font->draw(buf, core::rect<s32>(10,34,900,54), SColor(255,255,255,255));

        if (mesh) {
            int refs = mesh->getReferenceCount();
            int meshCacheSize = smgr->getMeshCache()->getMeshCount();
            std::sprintf(buf, "Mesh refs: %d  Mesh cache: %d", refs, meshCacheSize);
            if (font) font->draw(buf, core::rect<s32>(10,58,900,78), SColor(255,255,255,255));
        }

        // Instructions
        if (font) {
            font->draw(L"SPACE: Toggle load/unload", core::rect<s32>(10,82,900,102), SColor(255,255,255,255));
            font->draw(L"R: Reset memory baseline", core::rect<s32>(10,106,900,126), SColor(255,255,255,255));
            font->draw(L"ESC: Quit", core::rect<s32>(10,130,900,150), SColor(255,255,255,255));
        }

        driver->endScene();
    }

    // Final cleanup
    if (loaded) {
        unloadResources(smgr, driver, mesh, node, tex);
    }

    memMonitor.record("Before shutdown");
    memMonitor.analyzeStability();
    device->drop();

    std::printf("=== TEST COMPLETE ===\n");
    return 0;
}
**
If you are looking for people with whom to develop your game, even to try functionalities, I can help you, free. CC0 man.

Image
**
Post Reply