Hi Robert,

I have work on a small tool to detect how a program evolve during time,
It tracks the numer of instance of each object inherited from
osg::Referenced. I use this to detect 'leak' but release when quitting
the program so not really a leak  but to detect consumption of too much
memory during runtime.
The tool to works need modification in osg::Referenced. because there is
already a mecanism to set a custom DeleteHandler, I thought we could do
the same to intercept instanciation of osg::Referenced with a custom
InstanceHandler ?

In my use case, I have used a NodeCallBack that dump every second the
current instance of Referenced and Object, then I have used a function
to dump the gnuplot script from the data store each seconds.
Some screenshoot will help to understand how I use it:

http://plopbyte.net/tmp/combined.png
http://plopbyte.net/tmp/individual.png

here the data generated to build the graph
http://plopbyte.net/tmp/data.dat
http://plopbyte.net/tmp/plot.script


What do you think ?

I have attached my modified Referenced and Referenced.cpp to work with
my tool, but It would need to be more generic like the DeleteHandler to
enter in OSG.



Cheers,
Cedric

-- 
Provide OpenGL, WebGL and OpenSceneGraph services
+33 659 598 614 Cedric Pinson mailto:cedric.pin...@plopbyte.net
http://www.plopbyte.net
/* -*-c++-*- OpenSceneGraph - Copyright (C) 1998-2006 Robert Osfield 
 *
 * This library is open source and may be redistributed and/or modified under  
 * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or 
 * (at your option) any later version.  The full license is in LICENSE file
 * included with this distribution, and on the openscenegraph.org website.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 * OpenSceneGraph Public License for more details.
*/
#include <stdlib.h>

#include <osg/Referenced>
#include <osg/Notify>
#include <osg/ApplicationUsage>
#include <osg/Observer>

#include <typeinfo>
#include <memory>
#include <set>

#include <OpenThreads/ScopedLock>
#include <OpenThreads/Mutex>

#include <osg/DeleteHandler>
#include "ReferencedExtra.cpp"

namespace osg
{


//#define ENFORCE_THREADSAFE
//#define DEBUG_OBJECT_ALLOCATION_DESTRUCTION

// specialized smart pointer, used to get round auto_ptr<>'s lack of the destructor reseting itself to 0.
template<typename T>
struct ResetPointer
{
    ResetPointer():
        _ptr(0) {}

    ResetPointer(T* ptr):
        _ptr(ptr) {}

    ~ResetPointer()
    {
        delete _ptr;
        _ptr = 0;
    }

    inline ResetPointer& operator = (T* ptr)
    {
        if (_ptr==ptr) return *this;
        delete _ptr;
        _ptr = ptr;
        return *this;
    }

    void reset(T* ptr)
    {
        if (_ptr==ptr) return;
        delete _ptr;
        _ptr = ptr;
    }

    inline T& operator*()  { return *_ptr; }

    inline const T& operator*() const { return *_ptr; }

    inline T* operator->() { return _ptr; }

    inline const T* operator->() const   { return _ptr; }

    T* get() { return _ptr; }

    const T* get() const { return _ptr; }

    T* _ptr;
};


typedef ResetPointer<DeleteHandler> DeleteHandlerPointer;
typedef ResetPointer<OpenThreads::Mutex> GlobalMutexPointer;

OpenThreads::Mutex* Referenced::getGlobalReferencedMutex()
{
    static GlobalMutexPointer s_ReferencedGlobalMutext = new OpenThreads::Mutex;
    return s_ReferencedGlobalMutext.get();
}

// helper class for forcing the global mutex to be constructed when the library is loaded. 
struct InitGlobalMutexes
{
    InitGlobalMutexes()
    {
        Referenced::getGlobalReferencedMutex();
    }
};
static InitGlobalMutexes s_initGlobalMutexes;


#if !defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
static bool s_useThreadSafeReferenceCounting = getenv("OSG_THREAD_SAFE_REF_UNREF")!=0;
#endif
// static std::auto_ptr<DeleteHandler> s_deleteHandler(0);
static DeleteHandlerPointer s_deleteHandler(0);

static ApplicationUsageProxy Referenced_e0(ApplicationUsage::ENVIRONMENTAL_VARIABLE,"OSG_THREAD_SAFE_REF_UNREF","");

void Referenced::setThreadSafeReferenceCounting(bool enableThreadSafeReferenceCounting)
{
#if !defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    s_useThreadSafeReferenceCounting = enableThreadSafeReferenceCounting;
#endif
}

bool Referenced::getThreadSafeReferenceCounting()
{
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    return true;
#else
    return s_useThreadSafeReferenceCounting;
#endif
}


void Referenced::setDeleteHandler(DeleteHandler* handler)
{
    s_deleteHandler.reset(handler);
}

DeleteHandler* Referenced::getDeleteHandler()
{
    return s_deleteHandler.get();
}

#ifdef DEBUG_OBJECT_ALLOCATION_DESTRUCTION
OpenThreads::Mutex& getNumObjectMutex()
{
    static OpenThreads::Mutex s_numObjectMutex;
    return s_numObjectMutex;
}
static int s_numObjects = 0;
#endif

Referenced::Referenced(int dontlog):
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    _observerSet(0),
    _refCount(0)
#else
    _refMutex(0),
    _refCount(0),
    _observerSet(0)
#endif
{
#if !defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
#ifndef ENFORCE_THREADSAFE
    if (s_useThreadSafeReferenceCounting)
#endif
        _refMutex = new OpenThreads::Mutex;
#endif
        
#ifdef DEBUG_OBJECT_ALLOCATION_DESTRUCTION
    {
        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(getNumObjectMutex());
        ++s_numObjects;
        printf("Object created, total num=%d\n",s_numObjects);
    }
#endif
    _logged = !dontlog;
    if (!dontlog) {
        log(this);
    }
}

Referenced::Referenced(bool threadSafeRefUnref, int dontlog):
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    _observerSet(0),
    _refCount(0)
#else
    _refMutex(0),
    _refCount(0),
    _observerSet(0)
#endif
{
#if !defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
#ifndef ENFORCE_THREADSAFE
    if (threadSafeRefUnref)
#endif
        _refMutex = new OpenThreads::Mutex;
#endif

#ifdef DEBUG_OBJECT_ALLOCATION_DESTRUCTION
    {
        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(getNumObjectMutex());
        ++s_numObjects;
        printf("Object created, total num=%d\n",s_numObjects);
    }
#endif
    _logged = !dontlog;
    if (!dontlog)
        log(this);
}

Referenced::Referenced(const Referenced&, int dontlog):
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    _observerSet(0),
    _refCount(0)
#else
    _refMutex(0),
    _refCount(0),
    _observerSet(0)
#endif
{
#if !defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
#ifndef ENFORCE_THREADSAFE
    if (s_useThreadSafeReferenceCounting)
#endif
        _refMutex = new OpenThreads::Mutex;
#endif

#ifdef DEBUG_OBJECT_ALLOCATION_DESTRUCTION
    {
        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(getNumObjectMutex());
        ++s_numObjects;
        printf("Object created, total num=%d\n",s_numObjects);
    }
#endif
    _logged = !dontlog;
    if (!dontlog)
        log(this);
}

Referenced::~Referenced()
{
#ifdef DEBUG_OBJECT_ALLOCATION_DESTRUCTION
    {
        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(getNumObjectMutex());
        --s_numObjects;
        printf("Object created, total num=%d\n",s_numObjects);
    }
#endif

    if (_refCount>0)
    {
        OSG_WARN<<"Warning: deleting still referenced object "<<this<<" of type '"<<typeid(this).name()<<"'"<<std::endl;
        OSG_WARN<<"         the final reference count was "<<_refCount<<", memory corruption possible."<<std::endl;
    }

    // signal observers that we are being deleted.
    signalObserversAndDelete(true, false);

    // delete the ObserverSet
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    if (_observerSet.get()) static_cast<ObserverSet*>(_observerSet.get())->unref();
#else
    if (_observerSet) static_cast<ObserverSet*>(_observerSet)->unref();
#endif

#if !defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    if (_refMutex) delete _refMutex;
#endif

    if (_logged) {
        unlog(this);
    }

}

ObserverSet* Referenced::getOrCreateObserverSet() const
{
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    ObserverSet* observerSet = static_cast<ObserverSet*>(_observerSet.get());
    while (0 == observerSet)
    {
        ObserverSet* newObserverSet = new ObserverSet(this);
        newObserverSet->ref();

        if (!_observerSet.assign(newObserverSet, 0))
        {
            newObserverSet->unref();
        }

        observerSet = static_cast<ObserverSet*>(_observerSet.get());
    }
    return observerSet;
#else
    if (_refMutex)
    {
        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(*_refMutex);
        if (!_observerSet)
        {
            _observerSet = new ObserverSet(this);
            static_cast<ObserverSet*>(_observerSet)->ref();
        }
        return static_cast<ObserverSet*>(_observerSet);
    }
    else
    {
        if (!_observerSet) _observerSet = new ObserverSet(this);
        return static_cast<ObserverSet*>(_observerSet);
    }
#endif
}

void Referenced::addObserver(Observer* observer) const
{
    getOrCreateObserverSet()->addObserver(observer);
}

void Referenced::removeObserver(Observer* observer) const
{
    getOrCreateObserverSet()->removeObserver(observer);
}

void Referenced::signalObserversAndDelete(bool signalDelete, bool doDelete) const
{
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    ObserverSet* observerSet = static_cast<ObserverSet*>(_observerSet.get());
#else
    ObserverSet* observerSet = static_cast<ObserverSet*>(_observerSet);
#endif

    if (observerSet && signalDelete)
    {
        observerSet->signalObjectDeleted(const_cast<Referenced*>(this));
    }

    if (doDelete)
    {
        if (_refCount!=0) OSG_NOTICE<<"Warning Referenced::signalObserversAndDelete(,,) doing delete with _refCount="<<_refCount<<std::endl;

        if (getDeleteHandler()) deleteUsingDeleteHandler();
        else delete this;
    }
}


void Referenced::setThreadSafeRefUnref(bool threadSafe)
{
#if !defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    if (threadSafe)
    {
        if (!_refMutex)
        {
            // we want thread safe ref()/unref() so assign a mutex
            _refMutex = new OpenThreads::Mutex;
        }
    }
    else
    {
        if (_refMutex)
        {
            // we don't want thread safe ref()/unref() so remove any assigned mutex
            OpenThreads::Mutex* tmpMutexPtr = _refMutex;
            _refMutex = 0;
            delete tmpMutexPtr;
        }
    }
#endif
}

int Referenced::unref_nodelete() const
{
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    return --_refCount;
#else
    if (_refMutex)
    {
        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(*_refMutex);
        return --_refCount;
    }
    else
    {
        return --_refCount;
    }
#endif
}

void Referenced::deleteUsingDeleteHandler() const
{
    getDeleteHandler()->requestDelete(this);
}




} // end of namespace osg
#include <set>
#include <osg/observer_ptr>
#include <osg/Object>
#include <iostream>

namespace osg {

    static bool readEnv()
    {
        const char* ptr = getenv("OSG_LOG_OBJECT");
        if (ptr) {
            std::cout << "will log creation of object" << std::endl;
            return true;
        }
        return false;
    }
    static bool readEnvDumpOnConsole()
    {
        if (!readEnv())
            return false;
        const char* ptr = getenv("OSG_MEMORY_DUMP");
        if (ptr) {
            std::cout << "will dump on console" << std::endl;
            return true;
        }
        std::cout << "will not dump on console" << std::endl;
        return false;
    }
static bool s_log = readEnv();
static bool s_dump = readEnvDumpOnConsole();
OpenThreads::Mutex& getLogObjectMutex()
{
    static OpenThreads::Mutex my_numObjectMutex;
    return my_numObjectMutex;
}
typedef std::set<osg::Referenced*> ObjectObserver;
typedef std::map<std::string, int> TypeInstance;
static ObjectObserver s_allObjects;
static std::map<int, TypeInstance > s_toPlot;
void Referenced::reportCurrentMemoryObject()
{
    if (!s_log) 
        return;

    {
        static int nbCall = 0;
        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(getLogObjectMutex());
        if (s_dump)
            std::cout << "Total Num Objects allocated " << s_allObjects.size() << std::endl;
        TypeInstance& types = s_toPlot[nbCall];
        for (ObjectObserver::iterator it = s_allObjects.begin(); it != s_allObjects.end(); ++it) {
            osg::Referenced* obj = (*it);
            if (obj && obj->referenceCount() >= 1) {
                osg::Object* o = dynamic_cast<osg::Object*>(obj);
                if (o) {
                    std::string type = std::string(o->libraryName()) + ":" + std::string(o->className());
                    types[type]++;
                } else {
                    types[typeid(obj).name()]++;
                }
            }
        }
        if (s_dump) {
            for (TypeInstance::iterator it = types.begin(); it != types.end(); ++it)
                std::cout << it->first << " nb instances " << it->second << std::endl;
        }

        nbCall++;
    }
}

void Referenced::clearLog()
{
    OpenThreads::ScopedLock<OpenThreads::Mutex> lock(getLogObjectMutex());
    std::cout << "Clear log " << s_allObjects.size() << " entries" << std::endl;
    s_allObjects.clear();
}

static void gnuPlot(std::map<int, TypeInstance >& data)
{
    OpenThreads::ScopedLock<OpenThreads::Mutex> lock(getLogObjectMutex());
    // collect all type
    TypeInstance alltype;
    for (std::map<int, TypeInstance >::iterator it = data.begin(); it != data.end(); ++it) {
        TypeInstance& types = it->second;
        for (TypeInstance::iterator it2 = types.begin(); it2 != types.end(); ++it2)
            alltype[it2->first] = 0;
    }

    // need to finish here
    std::cout << "# sample ";
    for (TypeInstance::iterator it = alltype.begin(); it != alltype.end(); ++it)
        std::cout << "\t" << it->first;
    std::cout << std::endl;
        
    for (std::map<int, TypeInstance >::iterator it = data.begin(); it != data.end(); ++it) {
        TypeInstance& t = it->second;
        std::cout << it->first;
        for (TypeInstance::iterator it2 = alltype.begin(); it2 != alltype.end(); ++it2) {
            const std::string& key = it2->first;
            std::cout << "\t" << t[key];
        }
        std::cout << std::endl;
    }

    std::cout << "set term png size 1280,1024" << std::endl;
    std::cout << "set output \"combined.png\"" << std::endl;
    std::cout << "plot ";
    int i = 1;
    for (TypeInstance::iterator it = alltype.begin(); it != alltype.end(); ++it) {
        if (i == 1)
            std::cout << "\\" << std::endl;
        else
            std::cout << ",\\" << std::endl;
        std::cout << "\"data.dat\" u 1:" << ++i << " t \'" << it->first << "\' with lines";
    }
    std::cout << std::endl << std::endl;

    // draw each graph
    std::cout << "set term png size 1280,8192" << std::endl;
    std::cout << "set output \"individual.png\"" << std::endl;
    std::cout << "set tmargin 2" << std::endl << "set bmargin 2" << std::endl << "set lmargin 9" << std::endl << "set rmargin 2" << std::endl;
    std::cout << "set multiplot" << std::endl;
    int nbColumn = 2;
    double size = 1.0/(alltype.size()/nbColumn);
    std::cout << "set size " << 1.0/nbColumn << "," << size << std::endl;
    i = 1;
    int currentIndex = 0;
    for (TypeInstance::iterator it = alltype.begin(); it != alltype.end(); ++it) {
        int col = currentIndex/(alltype.size()/nbColumn);
        std::cout << "set origin " << col * 1.0 / nbColumn << "," << (currentIndex%(alltype.size()/nbColumn)) * 1.0 * size << std::endl;
        std::cout << "plot \"data.dat\" u 1:" << ++i << " t \'" << it->first << "\' with lines" << std::endl;
        currentIndex++;
    }
}

static void log(Referenced* obj)
{
    if (!s_log)
        return;
    {
    OpenThreads::ScopedLock<OpenThreads::Mutex> lock(getLogObjectMutex());
    s_allObjects.insert(obj);
    }
}

static void unlog(Referenced* obj)
{
    if (!s_log)
        return;
    {
    OpenThreads::ScopedLock<OpenThreads::Mutex> lock(getLogObjectMutex());
    s_allObjects.erase(obj);
    }
}
void Referenced::dumpStats()
{
    gnuPlot(s_toPlot);
}



}
/* -*-c++-*- OpenSceneGraph - Copyright (C) 1998-2006 Robert Osfield 
 *
 * This library is open source and may be redistributed and/or modified under  
 * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or 
 * (at your option) any later version.  The full license is in LICENSE file
 * included with this distribution, and on the openscenegraph.org website.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 * OpenSceneGraph Public License for more details.
*/

#ifndef OSG_REFERENCED
#define OSG_REFERENCED 1

#include <osg/Export>

#include <OpenThreads/ScopedLock>
#include <OpenThreads/Mutex>
#include <OpenThreads/Atomic>

#if !defined(_OPENTHREADS_ATOMIC_USE_MUTEX)
# define _OSG_REFERENCED_USE_ATOMIC_OPERATIONS
#endif

namespace osg {

// forward declare, declared after Referenced below.
class DeleteHandler;
class Observer;
class ObserverSet;

/** template class to help enforce static initialization order. */
template <typename T, T M()>
struct depends_on
{
    depends_on() { M(); }
};

/** Base class from providing referencing counted objects.*/
class OSG_EXPORT Referenced
{

    public:

        static void reportCurrentMemoryObject();
        static void clearLog();
        static void dumpStats();


        Referenced(int dontlog = 0); 
        
        explicit Referenced(bool threadSafeRefUnref, int dontlog = 0); 

        Referenced(const Referenced&, int dontlog = 0);

        inline Referenced& operator = (const Referenced&) { return *this; }

        /** Set whether to use a mutex to ensure ref() and unref() are thread safe.*/
        virtual void setThreadSafeRefUnref(bool threadSafe);

        /** Get whether a mutex is used to ensure ref() and unref() are thread safe.*/

#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
        bool getThreadSafeRefUnref() const { return true; }
#else
        bool getThreadSafeRefUnref() const { return _refMutex!=0; }
#endif

        /** Get the mutex used to ensure thread safety of ref()/unref(). */
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
        OpenThreads::Mutex* getRefMutex() const { return getGlobalReferencedMutex(); }
#else
        OpenThreads::Mutex* getRefMutex() const { return _refMutex; }
#endif

        /** Get the optional global Referenced mutex, this can be shared between all osg::Referenced.*/
        static OpenThreads::Mutex* getGlobalReferencedMutex();

        /** Increment the reference count by one, indicating that 
            this object has another pointer which is referencing it.*/
        inline int ref() const;
        
        /** Decrement the reference count by one, indicating that 
            a pointer to this object is referencing it.  If the
            reference count goes to zero, it is assumed that this object
            is no longer referenced and is automatically deleted.*/
        inline int unref() const;
        
        /** Decrement the reference count by one, indicating that 
            a pointer to this object is referencing it.  However, do
            not delete it, even if ref count goes to 0.  Warning, unref_nodelete() 
            should only be called if the user knows exactly who will
            be responsible for, one should prefer unref() over unref_nodelete() 
            as the later can lead to memory leaks.*/
        int unref_nodelete() const;
        
        /** Return the number pointers currently referencing this object. */
        inline int referenceCount() const { return _refCount; }


        /** Get the ObserverSet if one is attached, otherwise return NULL.*/
        ObserverSet* getObserverSet() const
        {
            #if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
                return static_cast<ObserverSet*>(_observerSet.get());
            #else
                return static_cast<ObserverSet*>(_observerSet);
            #endif
        }

        /** Get the ObserverSet if one is attached, otherwise create an ObserverSet, attach it, then return this newly created ObserverSet.*/
        ObserverSet* getOrCreateObserverSet() const;

        /** Add a Observer that is observing this object, notify the Observer when this object gets deleted.*/
        void addObserver(Observer* observer) const;

        /** remove Observer that is observing this object.*/
        void removeObserver(Observer* observer) const;

    public:

        /** Set whether reference counting should be use a mutex to create thread reference counting.*/
        static void setThreadSafeReferenceCounting(bool enableThreadSafeReferenceCounting);
        
        /** Get whether reference counting is active.*/
        static bool getThreadSafeReferenceCounting();

        friend class DeleteHandler;

        /** Set a DeleteHandler to which deletion of all referenced counted objects
          * will be delegated to.*/
        static void setDeleteHandler(DeleteHandler* handler);

        /** Get a DeleteHandler.*/
        static DeleteHandler* getDeleteHandler();

       
    protected:
    
        virtual ~Referenced();

        void signalObserversAndDelete(bool signalDelete, bool doDelete) const;

        void deleteUsingDeleteHandler() const;

        bool _logged;
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
        mutable OpenThreads::AtomicPtr  _observerSet;

        mutable OpenThreads::Atomic     _refCount;
#else
        
        mutable OpenThreads::Mutex*     _refMutex;

        mutable int                     _refCount;
        
        mutable void*                   _observerSet;
#endif
};

inline int Referenced::ref() const
{
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    return ++_refCount;
#else
    if (_refMutex)
    {
        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(*_refMutex); 
        return ++_refCount;
    }
    else
    {
        return ++_refCount;
    }
#endif
}

inline int Referenced::unref() const
{
    int newRef;
#if defined(_OSG_REFERENCED_USE_ATOMIC_OPERATIONS)
    newRef = --_refCount;
    bool needDelete = (newRef == 0);
#else
    bool needDelete = false;
    if (_refMutex)
    {
        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(*_refMutex); 
        newRef = --_refCount;
        needDelete = newRef==0;
    }
    else
    {
        newRef = --_refCount;
        needDelete = newRef==0;
    }
#endif

    if (needDelete)
    {
        signalObserversAndDelete(true,true);
    }
    return newRef;
}

// intrusive_ptr_add_ref and intrusive_ptr_release allow
// use of osg Referenced classes with boost::intrusive_ptr
inline void intrusive_ptr_add_ref(Referenced* p) { p->ref(); }
inline void intrusive_ptr_release(Referenced* p) { p->unref(); }

}

#endif

Attachment: signature.asc
Description: This is a digitally signed message part

_______________________________________________
osg-users mailing list
osg-users@lists.openscenegraph.org
http://lists.openscenegraph.org/listinfo.cgi/osg-users-openscenegraph.org

Reply via email to