m3g/m3gcore11/src/m3g_loader.c
author William Roberts <williamr@symbian.org>
Fri, 23 Jul 2010 11:46:39 +0100
changeset 126 0ee22b620a47
parent 0 5d03bc08d59c
child 116 171fae344dd4
permissions -rw-r--r--
Use buildrom ABI_DIR\BUILD_DIR conventions in the minigui .oby files

/*
* Copyright (c) 2003 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description: Native implementation of the Loader class
*
*/


/*!
 * \internal
 * \file
 * \brief Native implementation of the Loader class
 *
*/

#include "m3g_object.h"
#include "m3g_array.h"

/*----------------------------------------------------------------------
 * Internal data types
 *--------------------------------------------------------------------*/

/*!
 * \internal
 * \brief Possible global states for the loader
 */
typedef enum {
    /*! \internal \brief Loading not supported yet */
    LOADSTATE_NOT_SUPPORTED = -2,
    /*! \internal \brief Loading has terminated with an error */
    LOADSTATE_ERROR         = -1,
    /*! \internal \brief Loading has not started yet */
    LOADSTATE_INITIAL       =  0,
    /*! \internal \brief The identifier of the file is being read */
    LOADSTATE_IDENTIFIER,
    /*! \internal \brief The header of the section is being read */
    LOADSTATE_SECTION,
    /*! \internal \brief The header field of an object is being read */
    LOADSTATE_OBJECT,
    /*! \internal \brief Loading is finished */
    LOADSTATE_DONE
} LoaderState;

/*!
 * \internal
 * \brief Possible local states for the loader
 */
typedef enum {
    /*! \internal \brief Local state is entered */
    LOADSTATE_ENTER,
    /*! \internal \brief Local state is exited */
    LOADSTATE_EXIT,
    /*! \internal \brief Local state is section checksum */
    LOADSTATE_CHECKSUM
} LoaderLocalState;

/*!
 * \internal
 * \brief Buffered byte stream class
 */
typedef struct
{
    M3Gubyte *allocatedData;
    M3Gubyte *data;
    M3Gsizei capacity, bytesAvailable, totalBytes;
} BufferedStream;

/*!
 * \internal
 * \brief User data for a loaded object
 */
typedef struct
{
    M3GObject object;
    M3Gint numParams;
    M3Gbyte **params;
    M3Gsizei *paramLengths;
    M3Gint *paramId;
} UserData;

/*!
 * \internal
 * \brief Loader instance data
 */
typedef struct M3GLoaderImpl
{
    Object object;

    BufferedStream stream;
    M3Gsizei bytesRequired;
    M3Gsizei sectionBytesRequired;

    PointerArray refArray;
    PointerArray userDataArray;

    /*!
     * \internal
     * \brief The global state the loader is in
     *
     * This is a rather ordinary state machine thing; basically the
     * type of object being loaded, or one of the possible error
     * conditions. In here, it also amounts to a particular coroutine
     * execution context.
     */
    LoaderState state;

    /*!
     * \internal
     * \brief The local state of the loader
     *
     * This is basically the line number within a particular coroutine
     * function.
     */
    M3Gint localState;

    /*!
     * \internal
     * \brief Object being loaded
     */
    M3Gint objectType;

    /*!
     * \internal
     * \brief Loaded object
     */
    M3GObject loadedObject;

    /*!
     * \internal
     * \brief Pointer to the beginning of an object
     */
    M3Gubyte *objectData;

    /*!
     * \internal
     * \brief Pointer to the end of an object
     */
    M3Gubyte *objectDataEnd;

    /*!
     * \internal
     * \brief Pointer to the context data for the current coroutine
     * context
     */
    M3Gubyte *localData;

    /*!
     * \internal
     * \brief Size of the current coroutine data
     *
     * This is grown dynamically as necessary, rather than trying to
     * maintain a single size that fits all coroutines.
     */
    size_t localDataSize;

    /* File information */
    M3Gbool hasReferences;
    M3Gsizei fileSize;
    M3Gsizei contentSize;
    M3Gint triCount;
    M3Gint triConstraint;

    /* Section information */
    M3Gbool compressed;
    M3Gint sectionLength;
    M3Gint sectionNum;
    M3Gint inflatedLength;
    M3Gubyte *sectionData;
    M3Gubyte *allocatedSectionData;
    
    /* Adler data */
    M3Gint S12[2];
} Loader;

typedef struct {
    const unsigned char *data;
    int read;
    int length;
} compressedData;

/* Type ID used for classifying objects derived from Node */
#define ANY_NODE_CLASS ((M3GClass)(-1))

#include <string.h>
#define m3gCmp(s1, s2, len)  memcmp(s1, s2, len)

/*----------------------------------------------------------------------
 * Private constants
 *--------------------------------------------------------------------*/

#define M3G_MIN_OBJECT_SIZE     (1 + 4)
#define M3G_MIN_SECTION_SIZE    (1 + 4 + 4)
#define M3G_CHECKSUM_SIZE       4

#define M3G_ADLER_CONST         65521;

static const M3Gubyte M3G_FILE_IDENTIFIER[] = {
    0xAB, 0x4A, 0x53, 0x52, 0x31, 0x38, 0x34, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A
};

static const M3Gubyte PNG_FILE_IDENTIFIER[] = {
    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
};

static void m3gCleanupLoader(M3GLoader loader);

/*----------------------------------------------------------------------
 * Platform-specific "inflate" decompression code
 *--------------------------------------------------------------------*/

/*!
 * \internal
 * \brief Decompresses a block of data into an output buffer
 *
 * \param srcLength number of bytes in the input (compressed) buffer
 * \param src       pointer to the input buffer
 * \param dstLength number of bytes allocated in the output buffer
 * \param dst       pointer to the output buffer
 * \return the number of bytes written to \c dst
 */
static M3Gsizei m3gInflateBlock(M3Gsizei srcLength, const M3Gubyte *src,
                                M3Gsizei dstLength, M3Gubyte *dst);
                           
/* Include the platform-dependent implementation */
#if !defined(M3G_TARGET_GENERIC)
#   include "m3g_loader_inflate.inl"
#endif

/*----------------------------------------------------------------------
 * Private functions
 *--------------------------------------------------------------------*/

/*!
 * \internal
 * \brief Destructor
 */
static void m3gDestroyLoader(Object *obj)
{
    Loader* loader = (Loader *) obj;
    M3G_VALIDATE_OBJECT(loader);
    {
        Interface *m3g = M3G_INTERFACE(loader);
        M3Gint n, i;

        m3gCleanupLoader(loader);
        m3gDestroyArray(&loader->refArray, m3g);
        n = m3gArraySize(&loader->userDataArray);
        for (i = 0; i < n; ++i)
        {
            UserData *data = (UserData *)m3gGetArrayElement(&loader->userDataArray, i);
            m3gFree(m3g, data->params);
            m3gFree(m3g, data->paramLengths);
            m3gFree(m3g, data->paramId);
            m3gFree(m3g, data);
        }
        m3gDestroyArray(&loader->userDataArray, m3g);
        m3gFree(m3g, loader->stream.allocatedData);
        m3gFree(m3g, loader->allocatedSectionData);
    }
    m3gDestroyObject(obj);
}

/*!
 * \internal
 * \brief Stores new data in the stream buffer of this loader
 */
static M3Gbool m3gBufferData( M3GInterface m3g,
                           BufferedStream *stream,
                           M3Gsizei bytes,
                           const M3Gubyte *data)
{
    M3Gsizei used;

    /* Allocate initial buffer */
    if (stream->allocatedData == NULL) {
        stream->capacity = bytes + 512;
        stream->allocatedData = m3gAllocZ(m3g, stream->capacity);
        if (!stream->allocatedData) {
            return M3G_FALSE;
        }
        stream->data = stream->allocatedData;
        stream->bytesAvailable = 0;
        stream->totalBytes = 0;
    }

    /* First skip used bytes */
    used = stream->data - stream->allocatedData;
    if (used > 0) {
        m3gMove(stream->allocatedData, stream->data, stream->bytesAvailable);
        stream->data = stream->allocatedData;
    }

    /* Check if new data fits in current buffer */
    if ((stream->capacity - stream->bytesAvailable) < bytes) {
        M3Gubyte *newData;
        stream->capacity = stream->capacity + bytes + 512;
        newData = m3gAllocZ(m3g, stream->capacity);
        if (!newData) {
            m3gFree(m3g, stream->allocatedData);
            stream->allocatedData = NULL;
            return M3G_FALSE;
        }
        m3gCopy(newData, stream->data, stream->bytesAvailable);
        m3gFree(m3g, stream->allocatedData);
        stream->allocatedData = newData;
        stream->data = stream->allocatedData;
    }

    m3gCopy(stream->data + stream->bytesAvailable, data, bytes);
    stream->bytesAvailable += bytes;

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Resets buffered data
 */
static void m3gResetBufferedData(BufferedStream *stream)
{
    stream->bytesAvailable = 0;
    stream->data = stream->allocatedData;
    stream->totalBytes = 0;
}

/*!
 * \internal
 * \brief Gets buffered data pointer
 */
static M3Gubyte *m3gGetBufferedDataPtr(BufferedStream *stream, M3Gint length)
{
    if (stream->bytesAvailable >= length) {
        return stream->data;
    }
    else {
        return NULL;
    }
}

/*!
 * \internal
 * \brief Advances buffered data pointer
 */
static void m3gAdvanceBufferedData(BufferedStream *stream, M3Gint length)
{
    stream->data += length;
    stream->bytesAvailable -= length;
    stream->totalBytes += length;
}

/*!
 * \internal
 * \brief Verify a boolean
 */
static M3Gbool m3gVerifyBool(M3Gubyte *data)
{
    return (*data == 0 || *data == 1);
}

/*!
 * \internal
 * \brief Loads ARGB color from data array
 */
static M3Guint m3gLoadARGB(M3Gubyte *data)
{
    M3Guint v = data[3];
    v <<= 8;
    v |=  data[0];
    v <<= 8;
    v |=  data[1];
    v <<= 8;
    v |=  data[2];

    return v;
}

/*!
 * \internal
 * \brief Loads RGB color from data array
 */
static M3Guint m3gLoadRGB(M3Gubyte *data)
{
    M3Guint v = data[0];
    v <<= 8;
    v |=  data[1];
    v <<= 8;
    v |=  data[2];

    return v;
}

/*!
 * \internal
 * \brief Loads short from data array
 */
static M3Gshort m3gLoadShort(M3Gubyte *data)
{
    M3Gshort v = data[1];
    v <<= 8;
    v |=  data[0];

    return v;
}

/*!
 * \internal
 * \brief Loads integer from data array
 */
static M3Gint m3gLoadInt(M3Gubyte *data)
{
    M3Gint v = data[3];
    v <<= 8;
    v |=  data[2];
    v <<= 8;
    v |=  data[1];
    v <<= 8;
    v |=  data[0];

    return v;
}

/*!
 * \internal
 * \brief Loads integer from data array
 */
static M3Gbool m3gLoadFloat(M3Gubyte *data, M3Gfloat *res)
{
    M3Guint v = data[3];
    v <<= 8;
    v |=  data[2];
    v <<= 8;
    v |=  data[1];
    v <<= 8;
    v |=  data[0];

    *res = (*(M3Gfloat*)&(v));
    if ((v & 0x7f800000) ==  0x7f800000 ||
        v == 0x80000000 || // negative zero
        ((v & 0x007FFFFF ) != 0 && ( v & 0x7F800000 ) == 0))
        return M3G_FALSE;
    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads 4 * 4 matrix from data array
 */
static M3Gbool m3gLoadMatrix(Matrix *m, M3Gubyte *data)
{
    M3Gint i;
    M3Gfloat array[16];

    for (i = 0; i < 16; i++) {
        if (!m3gLoadFloat(data + 4 * i, &array[i]))
            return M3G_FALSE;
    }

    m3gSetMatrixRows(m, array);
    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Inflates a section
 */
static M3Gubyte *m3gInflateSection(Loader *loader,
                                   M3Gubyte *compressed,
                                   M3Gint cLength, M3Gint iLength)
{
    M3Gubyte *inflated = m3gAllocZ(M3G_INTERFACE(loader), iLength);
    if (inflated && !m3gInflateBlock(cLength, compressed, iLength, inflated)) {
        m3gFree(M3G_INTERFACE(loader), inflated);
        return NULL;
    }

    return inflated;
}

/*!
 * \internal
 * \brief Loads file identifier
 */
static LoaderState m3gLoadIdentifier(Loader *loader)
{
    M3Gubyte *data = m3gGetBufferedDataPtr(&loader->stream, loader->bytesRequired);

    if (loader->localState == LOADSTATE_ENTER) {
        if (m3gCmp(data, PNG_FILE_IDENTIFIER, sizeof(PNG_FILE_IDENTIFIER)) == 0) {
            m3gAdvanceBufferedData(&loader->stream, loader->bytesRequired);
            return LOADSTATE_NOT_SUPPORTED;
        }
        else {
            loader->localState = LOADSTATE_EXIT;
            loader->bytesRequired = sizeof(M3G_FILE_IDENTIFIER);
            return LOADSTATE_IDENTIFIER;
        }
    }
    else {
        if (m3gCmp(data, M3G_FILE_IDENTIFIER, sizeof(M3G_FILE_IDENTIFIER)) == 0) {
            m3gAdvanceBufferedData(&loader->stream, loader->bytesRequired);
            loader->localState = LOADSTATE_ENTER;
            loader->bytesRequired = M3G_MIN_SECTION_SIZE;
            return LOADSTATE_SECTION;
        }

        loader->bytesRequired = 0;
        return LOADSTATE_ERROR;
    }
}

/*!
 * \internal
 * \brief Adler helper functions
 */

static void m3gInitAdler(M3Gint *S12)
{
    S12[0] = 1; S12[1] = 0;
}

static void m3gUpdateAdler(M3Gint *S12, M3Gubyte *data, M3Gint length)
{
    int i;
    for (i = 0; i < length; i++) {
        S12[0] = (S12[0] + data[i]) % M3G_ADLER_CONST;
        S12[1] = (S12[1] + S12[0])  % M3G_ADLER_CONST;
    }
}

static M3Gint m3gGetAdler(M3Gint *S12)
{
    return (S12[1] << 16) | S12[0];
}

/*!
 * \internal
 * \brief Loads a section
 */
static LoaderState m3gLoadSection(Loader *loader)
{
    M3Gubyte *data = m3gGetBufferedDataPtr(&loader->stream, loader->bytesRequired);

    if (data == NULL) {
        m3gRaiseError(M3G_INTERFACE(loader), M3G_IO_ERROR);
        return LOADSTATE_ERROR;
    }

    switch(loader->localState) {
    case LOADSTATE_ENTER:
        m3gAdvanceBufferedData(&loader->stream, loader->bytesRequired);
        m3gInitAdler(loader->S12);
        m3gUpdateAdler(loader->S12, data, loader->bytesRequired);

        if (*data > 1)
        {
            m3gRaiseError(M3G_INTERFACE(loader), M3G_IO_ERROR);
            return LOADSTATE_ERROR;
        }
        loader->compressed = data[0];
        loader->sectionLength = m3gLoadInt(data + 1);
        loader->inflatedLength = m3gLoadInt(data + 5);

        loader->localState = LOADSTATE_EXIT;
        loader->bytesRequired = loader->sectionLength - loader->bytesRequired;
        if (!loader->compressed && loader->inflatedLength != (loader->bytesRequired - M3G_CHECKSUM_SIZE))
        {
            m3gRaiseError(M3G_INTERFACE(loader), M3G_IO_ERROR);
            return LOADSTATE_ERROR;
        }

        loader->sectionNum++;

        /* Special case for empty sections */
        if (loader->bytesRequired == M3G_CHECKSUM_SIZE) {
            loader->localData = data + loader->sectionLength - M3G_CHECKSUM_SIZE;
            loader->sectionData = loader->localData;
            loader->compressed = M3G_FALSE;
            loader->localState = LOADSTATE_CHECKSUM;
        }
        return LOADSTATE_SECTION;

    case LOADSTATE_EXIT:
    default:
        m3gUpdateAdler(loader->S12, data, loader->bytesRequired - M3G_CHECKSUM_SIZE);

        if (loader->compressed) {
            if (loader->inflatedLength > 0) {
                m3gFree(M3G_INTERFACE(loader), loader->allocatedSectionData);
                loader->sectionData = m3gInflateSection(loader, data, loader->bytesRequired, loader->inflatedLength);
                loader->allocatedSectionData = loader->sectionData;

                if (!loader->sectionData) {
                    if (m3gErrorRaised(M3G_INTERFACE(loader)) == M3G_NO_ERROR)
                        m3gRaiseError(M3G_INTERFACE(loader), M3G_IO_ERROR);
                    return LOADSTATE_ERROR;
                }
            }
            else {
                loader->sectionData = NULL;
            }
        }
        else {
            loader->sectionData = data;
        }

        loader->localData = loader->sectionData;
        loader->sectionBytesRequired = M3G_MIN_OBJECT_SIZE;
        loader->localState = LOADSTATE_ENTER;
        return LOADSTATE_OBJECT;

    case LOADSTATE_CHECKSUM:
        if (loader->localData != loader->sectionData + loader->inflatedLength || /* Length */
            m3gLoadInt(data + loader->bytesRequired - M3G_CHECKSUM_SIZE) != m3gGetAdler(loader->S12))
        {
            m3gRaiseError(M3G_INTERFACE(loader), M3G_IO_ERROR);
            m3gFree(M3G_INTERFACE(loader), loader->allocatedSectionData);
            loader->allocatedSectionData = NULL;
            return LOADSTATE_ERROR;
        }
        m3gAdvanceBufferedData(&loader->stream, loader->bytesRequired);

        m3gFree(M3G_INTERFACE(loader), loader->allocatedSectionData);
        loader->allocatedSectionData = NULL;

        loader->localState = LOADSTATE_ENTER;
        loader->bytesRequired = M3G_MIN_SECTION_SIZE;
        return LOADSTATE_SECTION;
    }
}

/*!
 * \internal
 * \brief Resets section data pointer to the beginning of an object
 */
static void m3gRewindObject(Loader *loader)
{
    loader->localData = loader->objectData;
}

/*!
 * \internal
 * \brief Resets section data pointer to the end of an object
 */
static void m3gSkipObject(Loader *loader)
{
    loader->localData = loader->objectDataEnd;
}

/*!
 * \internal
 * \brief Marks object to begin
 */
static void m3gBeginObject(Loader *loader)
{
    loader->objectData = loader->localData;
}

/*!
 * \internal
 * \brief Marks object to end
 */
static void m3gEndObject(Loader *loader)
{
    loader->objectDataEnd = loader->localData;
}

/*!
 * \internal
 * \brief Gets section data pointer
 */
static M3Gubyte *m3gGetSectionDataPtr(Loader *loader, M3Gint length)
{
    if ((loader->localData + length) <= (loader->sectionData + loader->inflatedLength)) {
        return loader->localData;
    }
    else {
        return NULL;
    }
}

/*!
 * \internal
 * \brief Advances section data pointer
 */
static void m3gAdvanceSectionData(Loader *loader, M3Gint length)
{
    loader->localData += length;
}

/*!
 * \internal
 * \brief Check length of the available section data
 */
static M3Gbool m3gCheckSectionDataLength(Loader *loader, const M3Gubyte *data, M3Gsizei length)
{
    if (data + length < data) return M3G_FALSE; /* Check for overflow */
    return ((data + length) <= (loader->sectionData + loader->inflatedLength));
}

/*!
 * \internal
 * \brief References an object in the object array
 *
 * \note Uses lowest bit of the pointer to mark a reference
 */
static void m3gReferenceLoaded(PointerArray *array, M3Gint idx)
{
    M3Guint ptr = (M3Guint)m3gGetArrayElement(array, idx);
    ptr |= 1;
    m3gSetArrayElement(array, idx, (void *)ptr);
}

/*!
 * \internal
 * \brief Gets an object in the object array and
 * returns reference status
 */
static M3GObject m3gGetLoadedPtr(PointerArray *array, M3Gint idx, M3Gbool *referenced)
{
    M3Guint ptr = (M3Guint)m3gGetArrayElement(array, idx);
    if (referenced != NULL) {
        *referenced = ptr & 1;
    }
    return (M3GObject)(ptr & (~1));
}

/*!
 * \internal
 * \brief Gets a loaded object and marks it referenced
 */
static M3GObject m3gGetLoaded(Loader *loader, M3Gint idx, M3GClass classID)
{
    M3GObject obj;
    M3GClass objClassID;
    M3Gbool isCompatible;

    if (idx == 0) return NULL;
    idx -= 2;

    if (idx < 0 || idx >= m3gArraySize(&loader->refArray)) {
        /* Error, not loaded */
        m3gRaiseError(M3G_INTERFACE(loader), M3G_IO_ERROR);
        return NULL;
    }

    obj = m3gGetLoadedPtr(&loader->refArray, idx, NULL);
    objClassID = M3G_CLASS(obj);

    /* Class type check; handle nodes as a special case */
    
    if (classID == ANY_NODE_CLASS) {
        switch (objClassID) {
        case M3G_CLASS_CAMERA:
        case M3G_CLASS_GROUP:
        case M3G_CLASS_LIGHT:
        case M3G_CLASS_MESH:
        case M3G_CLASS_MORPHING_MESH:
        case M3G_CLASS_SKINNED_MESH:
        case M3G_CLASS_SPRITE:
        case M3G_CLASS_WORLD:
            isCompatible = M3G_TRUE;
            break;
        default:
            isCompatible = M3G_FALSE;
        }
    }
    else {
        switch (classID) {
        case M3G_ABSTRACT_CLASS:
            M3G_ASSERT(M3G_FALSE);
            isCompatible = M3G_FALSE;
            break;
        case M3G_CLASS_MESH:
            isCompatible = (classID == M3G_CLASS_MESH)
                || (classID == M3G_CLASS_MORPHING_MESH)
                || (classID == M3G_CLASS_SKINNED_MESH);
            break;
        default:
            isCompatible = (classID == objClassID);
        }
    }

    if (!isCompatible) {
        /* Error, class mismatch */
        m3gRaiseError(M3G_INTERFACE(loader), M3G_IO_ERROR);
        return NULL;
    }

    /* Mark object as referenced */
    m3gReferenceLoaded(&loader->refArray, idx);
    return obj;
}

/*!
 * \internal
 * \brief Loads Object3D data
 */
static M3Gbool m3gLoadObject3DData(Loader *loader, M3GObject obj)
{
    M3Guint animTracks, i, userParams, paramId, paramLength;
    UserData *userData = NULL;
    M3Gubyte *data = m3gGetSectionDataPtr(loader, 8);
    if (data == NULL) return M3G_FALSE;

    m3gSetUserID(obj, m3gLoadInt(data));
    data += 4;

    animTracks = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (animTracks >= 0x1fffffff) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, animTracks * 4 + 4) == M3G_FALSE) return M3G_FALSE;
    for (i = 0; i < animTracks; i++) {
        M3GAnimationTrack at = (M3GAnimationTrack)m3gGetLoaded(loader, m3gLoadInt(data),M3G_CLASS_ANIMATION_TRACK);
        if (at == NULL || m3gAddAnimationTrack(obj, at) == -1) {
            return M3G_FALSE;
        }
        data += 4;
    }

    userParams = m3gLoadInt(data);
    data += 4;

    if (userParams != 0) {
        /* Overflow? */
        if (userParams >= 0x10000000) {
            return M3G_FALSE;
        }

        if (m3gCheckSectionDataLength(loader, data, userParams * 8) == M3G_FALSE)
            return M3G_FALSE; /* Check the minimum size to avoid useless allocation */
        userData = (UserData *)m3gAllocZ(M3G_INTERFACE(loader), sizeof(UserData));
        if (userData == NULL)
            return M3G_FALSE;
        userData->object = obj;
        userData->numParams = userParams;
        userData->params = (M3Gbyte **)m3gAllocZ(M3G_INTERFACE(loader), userParams*sizeof(M3Gbyte *));
        userData->paramLengths = (M3Gsizei *)m3gAlloc(M3G_INTERFACE(loader), userParams*sizeof(M3Gsizei));
        userData->paramId = (M3Gint *)m3gAlloc(M3G_INTERFACE(loader), userParams*sizeof(M3Gint));
        if (userData->params == NULL ||
            userData->paramLengths == NULL ||
            userData->paramId == NULL ||
            m3gArrayAppend(&loader->userDataArray, userData, M3G_INTERFACE(loader)) == -1) {
            m3gFree(M3G_INTERFACE(loader), userData->params);
            m3gFree(M3G_INTERFACE(loader), userData->paramLengths);
            m3gFree(M3G_INTERFACE(loader), userData->paramId);
            m3gFree(M3G_INTERFACE(loader), userData);
            return M3G_FALSE;
        }

        for (i = 0; i < userParams; i++) {
            if (m3gCheckSectionDataLength(loader, data, 8) == M3G_FALSE) return M3G_FALSE;
            paramId = m3gLoadInt(data);
            data += 4;
            paramLength = m3gLoadInt(data);
            data += 4;
            userData->paramId[i] = paramId;
            userData->paramLengths[i] = paramLength;
            if (m3gCheckSectionDataLength(loader, data, paramLength) == M3G_FALSE) return M3G_FALSE;
            userData->params[i] = (M3Gbyte *)m3gAlloc(M3G_INTERFACE(loader), paramLength*sizeof(M3Gbyte));
            if (userData->params[i] == NULL)
                return M3G_FALSE;
            m3gCopy(userData->params[i], data, paramLength);
            data += paramLength;
        }
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));
    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Skips Object3D data
 */
static M3Gbool m3gSkipObject3DData(Loader *loader)
{
    M3Guint animTracks, i, userParams, paramLength;
    M3Gubyte *data = m3gGetSectionDataPtr(loader, 8);
    if (data == NULL) return M3G_FALSE;

    data += 4;
    animTracks = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (animTracks >= 0x1fffffff) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, animTracks * 4 + 4) == M3G_FALSE) return M3G_FALSE;
    for (i = 0; i < animTracks; i++) {
        data += 4;
    }

    userParams = m3gLoadInt(data);
    data += 4;

    for (i = 0; i < userParams; i++) {
        if (m3gCheckSectionDataLength(loader, data, 8) == M3G_FALSE) return M3G_FALSE;
        data += 4;
        paramLength = m3gLoadInt(data);
        if (m3gCheckSectionDataLength(loader, data, paramLength) == M3G_FALSE) return M3G_FALSE;
        data += 4 + paramLength;
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));
    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads transformable data
 */
static M3Gbool m3gLoadTransformableData(Loader *loader, M3GTransformable obj)
{
    M3Gfloat f1, f2, f3, f4;
    M3Gubyte *data;
    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 1);
    if (data == NULL) return M3G_FALSE;

    if (!m3gVerifyBool(data)) return M3G_FALSE;
    /* Component transform ? */
    if (*data++) {
        if (m3gCheckSectionDataLength(loader, data, 40) == M3G_FALSE) return M3G_FALSE;
        if (!m3gLoadFloat(data + 0, &f1) ||
            !m3gLoadFloat(data + 4, &f2) ||
            !m3gLoadFloat(data + 8, &f3))
            return M3G_FALSE;
        m3gSetTranslation(obj, f1, f2, f3);
        if (!m3gLoadFloat(data + 12, &f1) ||
            !m3gLoadFloat(data + 16, &f2) ||
            !m3gLoadFloat(data + 20, &f3))
            return M3G_FALSE;
        m3gSetScale(obj, f1, f2, f3);
        if (!m3gLoadFloat(data + 24, &f1) ||
            !m3gLoadFloat(data + 28, &f2) ||
            !m3gLoadFloat(data + 32, &f3) ||
            !m3gLoadFloat(data + 36, &f4))
            return M3G_FALSE;
        m3gSetOrientation(obj, f1, f2, f3, f4);
        data += 40;
    }

    if (m3gCheckSectionDataLength(loader, data, 1) == M3G_FALSE) return M3G_FALSE;
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    /* Generic transform */
    if (*data++) {
        Matrix m;
        if (m3gCheckSectionDataLength(loader, data, 64) == M3G_FALSE) return M3G_FALSE;
        if (!m3gLoadMatrix(&m, data)) return M3G_FALSE;
        m3gSetTransform(obj, &m);
        data += 64;
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));
    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Skips transformable data
 */
static M3Gbool m3gSkipTransformableData(Loader *loader)
{
    M3Gubyte *data;
    if (!m3gSkipObject3DData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 1);
    if (data == NULL) return M3G_FALSE;

    /* Component transform ? */
    if (*data++) {
        if (m3gCheckSectionDataLength(loader, data, 40) == M3G_FALSE) return M3G_FALSE;
        data += 40;
    }

    if (m3gCheckSectionDataLength(loader, data, 1) == M3G_FALSE) return M3G_FALSE;
    /* Generic transform */
    if (*data++) {
        if (m3gCheckSectionDataLength(loader, data, 64) == M3G_FALSE) return M3G_FALSE;
        data += 64;
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));
    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads node data
 */
static M3Gbool m3gLoadNodeData(Loader *loader, M3GNode obj)
{
    M3Gubyte *data;
    if (!m3gLoadTransformableData(loader, (M3GTransformable)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 8);
    if (data == NULL) return M3G_FALSE;

    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gEnable(obj, 0, *data++);
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gEnable(obj, 1, *data++);
    {
        unsigned a = *data++; 
        m3gSetAlphaFactor(obj, m3gDivif(a, 255));
    }
    m3gSetScope(obj, m3gLoadInt(data));
    data += 4;

    if (!m3gVerifyBool(data)) return M3G_FALSE;
    if (*data++) {
        M3Gubyte zt, yt;
        M3Gint zr, yr;
        if (m3gCheckSectionDataLength(loader, data, 10) == M3G_FALSE) return M3G_FALSE;
        zt = *data++;
        yt = *data++;
        zr = m3gLoadInt(data);
        yr = m3gLoadInt(data + 4);
        m3gSetAlignment(obj,    (M3GNode)m3gGetLoaded(loader, zr, ANY_NODE_CLASS),
                                zt,
                                (M3GNode)m3gGetLoaded(loader, yr, ANY_NODE_CLASS),
                                yt);
        data += 8;
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));
    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Skips node data
 */
static M3Gbool m3gSkipNodeData(Loader *loader)
{
    M3Gubyte *data;
    if (!m3gSkipTransformableData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 8);
    if (data == NULL) return M3G_FALSE;
    data += 7;

    /* Alignment? */
    if (*data++) {
        if (m3gCheckSectionDataLength(loader, data, 10) == M3G_FALSE) return M3G_FALSE;
        data += 10;
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));
    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a camera
 */
static M3Gbool m3gLoadCamera(Loader *loader)
{
    M3Gfloat f1, f2, f3, f4;
    M3Gubyte *data;
    M3GCamera obj = m3gCreateCamera(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadNodeData(loader, (M3GNode)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 1);
    if (data == NULL) return M3G_FALSE;

    switch(*data++) {
    case M3G_GENERIC:
        {
            Matrix m;
            if (m3gCheckSectionDataLength(loader, data, 64) == M3G_FALSE) return M3G_FALSE;
            if (!m3gLoadMatrix(&m, data)) return M3G_FALSE;
            m3gSetProjectionMatrix(obj, &m);
            data += 64;
        }
        break;
    case M3G_PERSPECTIVE:
        if (m3gCheckSectionDataLength(loader, data, 16) == M3G_FALSE) return M3G_FALSE;
        if (!m3gLoadFloat(data + 0, &f1) ||
            !m3gLoadFloat(data + 4, &f2) ||
            !m3gLoadFloat(data + 8, &f3) ||
            !m3gLoadFloat(data + 12, &f4))
            return M3G_FALSE;
        m3gSetPerspective(obj, f1, f2, f3, f4);
        data += 16;
        break;
    case M3G_PARALLEL:
        if (m3gCheckSectionDataLength(loader, data, 16) == M3G_FALSE) return M3G_FALSE;
        if (!m3gLoadFloat(data + 0, &f1) ||
            !m3gLoadFloat(data + 4, &f2) ||
            !m3gLoadFloat(data + 8, &f3) ||
            !m3gLoadFloat(data + 12, &f4))
            return M3G_FALSE;
        m3gSetParallel(obj, f1, f2, f3, f4);
        data += 16;
        break;
    default:
        /* Error */
        break;
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a background
 */
static M3Gbool m3gLoadBackground(Loader *loader)
{
    M3Gubyte *data;
    M3GBackground obj = m3gCreateBackground(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 28);
    if (data == NULL) return M3G_FALSE;

    m3gSetBgColor(obj, m3gLoadARGB(data));
    data += 4;
    m3gSetBgImage(obj, (M3GImage)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_IMAGE));
    data += 4;
    m3gSetBgMode(obj, data[0], data[1]);
    data += 2;
    m3gSetBgCrop(obj,   m3gLoadInt(data),
                        m3gLoadInt(data + 4),
                        m3gLoadInt(data + 8),
                        m3gLoadInt(data + 12) );
    data += 16;

    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gSetBgEnable(obj, 0, *data++);
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gSetBgEnable(obj, 1, *data++);

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a vertex array
 */
static M3Gbool m3gLoadVertexArray(Loader *loader)
{
    M3Gint i, j;
    M3Guint size;
    M3Gushort vertices;
    M3Gubyte *data, components, encoding;
    M3Gdatatype componentSize;
    M3GVertexArray obj;

    m3gBeginObject(loader);
    if (!m3gSkipObject3DData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 5);
    if (data == NULL) return M3G_FALSE;

    if (*data != 1 && *data != 2) return M3G_FALSE;
    componentSize = (*data++ == 1) ? M3G_BYTE : M3G_SHORT;
    components    = *data++;
    encoding      = *data++;
    vertices      = m3gLoadShort(data);
    data += 2;

    size = vertices * components * (componentSize == M3G_BYTE ? 1 : 2);

    /* Overflow? */
    if (size < vertices) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, size) == M3G_FALSE) return M3G_FALSE;
    obj = m3gCreateVertexArray(M3G_INTERFACE(loader), vertices, components, componentSize);
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (componentSize == M3G_BYTE) {
        M3Gbyte previousValues[4];
        m3gZero(previousValues, sizeof(previousValues));

        for (i = 0; i < vertices; i++) {
            for (j = 0; j < components; j++) {
                if (encoding == 0) {
                    previousValues[j] = *data++;
                }
                else {
                    previousValues[j] = (M3Gbyte) (previousValues[j] + *data++);
                }
            }
            m3gSetVertexArrayElements(obj, i, 1, sizeof(previousValues), componentSize, previousValues);
        }
    }
    else {
        M3Gshort previousValues[4];
        m3gZero(previousValues, sizeof(previousValues));

        for (i = 0; i < vertices; i++) {
            for (j = 0; j < components; j++) {
                if (encoding == 0) {
                    previousValues[j] = m3gLoadShort(data);
                }
                else {
                    previousValues[j] = (M3Gshort) (previousValues[j] + m3gLoadShort(data));
                }
                data += 2;
            }
            m3gSetVertexArrayElements(obj, i, 1, sizeof(previousValues), componentSize, previousValues);
        }
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    m3gEndObject(loader);
    m3gRewindObject(loader);
    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }
    m3gSkipObject(loader);

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a vertex buffer
 */
static M3Gbool m3gLoadVertexBuffer(Loader *loader)
{
    M3Gubyte *data;
    M3GVertexArray va;
    M3Gfloat bias[3], scale;
    M3Guint i, taCount;
    M3GVertexBuffer obj = m3gCreateVertexBuffer(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 36);
    if (data == NULL) return M3G_FALSE;

    m3gSetVertexDefaultColor(obj, m3gLoadARGB(data));
    data += 4;

    /* Positions */
    va = (M3GVertexArray)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_VERTEX_ARRAY);
    data += 4;
    if (!m3gLoadFloat(data + 0, &bias[0])) return M3G_FALSE;
    if (!m3gLoadFloat(data + 4, &bias[1])) return M3G_FALSE;
    if (!m3gLoadFloat(data + 8, &bias[2])) return M3G_FALSE;
    if (!m3gLoadFloat(data + 12, &scale)) return M3G_FALSE;
    if (va != NULL) {
        m3gSetVertexArray(obj, va, scale, bias, 3);
    }
    data += 16;

    /* Normals */
    va = (M3GVertexArray)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_VERTEX_ARRAY);
    data += 4;
    if (va != NULL) {
        m3gSetNormalArray(obj, va);
    }

    /* Colors */
    va = (M3GVertexArray)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_VERTEX_ARRAY);
    data += 4;
    if (va != NULL) {
        m3gSetColorArray(obj, va);
    }

    /* Texture coordinates */
    taCount = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (taCount >= 0x0ccccccc) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, taCount * 20) == M3G_FALSE) return M3G_FALSE;
    for (i = 0; i < taCount; i++) {
        va = (M3GVertexArray)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_VERTEX_ARRAY);
        data += 4;
        if (!m3gLoadFloat(data + 0, &bias[0])) return M3G_FALSE;
        if (!m3gLoadFloat(data + 4, &bias[1])) return M3G_FALSE;
        if (!m3gLoadFloat(data + 8, &bias[2])) return M3G_FALSE;
        if (!m3gLoadFloat(data + 12, &scale)) return M3G_FALSE;
        if (va != NULL) {
            m3gSetTexCoordArray(obj, i, va, scale, bias, 3);
        }
        else {
            return M3G_FALSE;
        }
        data += 16;
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}


/*!
 * \internal
 * \brief Loads a triangle strip array
 */
static M3Gbool m3gLoadTsa(Loader *loader)
{
    M3Gubyte *data;
    M3GIndexBuffer obj = 0;
    M3Gubyte encoding;
    M3Guint startIndex = 0, indicesCount = 0, *indices = NULL;
    M3Guint lengthCount;
    M3Gint *lengths = NULL;
    M3Guint i;

    m3gBeginObject(loader);
    if (!m3gSkipObject3DData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 1);
    if (data == NULL) return M3G_FALSE;

    encoding = *data++;

    switch(encoding) {
    case 0:
        if (m3gCheckSectionDataLength(loader, data, 4) == M3G_FALSE) return M3G_FALSE;
        startIndex = m3gLoadInt(data);
        data += 4;
        break;

    case 1:
        if (m3gCheckSectionDataLength(loader, data, 1) == M3G_FALSE) return M3G_FALSE;
        startIndex = *data++;
        break;

    case 2:
        if (m3gCheckSectionDataLength(loader, data, 2) == M3G_FALSE) return M3G_FALSE;
        startIndex = (M3Gushort) m3gLoadShort(data);
        data += 2;
        break;

    case 128:
        if (m3gCheckSectionDataLength(loader, data, 4) == M3G_FALSE) return M3G_FALSE;
        indicesCount = m3gLoadInt(data);
        data += 4;

        /* Overflow? */
        if (indicesCount >= 0x20000000) {
            return M3G_FALSE;
        }

        if (m3gCheckSectionDataLength(loader, data, indicesCount * 4) == M3G_FALSE) return M3G_FALSE;
        indices = m3gAllocZ(M3G_INTERFACE(loader), sizeof(M3Gint) * indicesCount);
        if (!indices) return M3G_FALSE;
        for (i = 0; i < indicesCount; i++ ) {
            indices[i] = m3gLoadInt(data);
            data += 4;
        }
        break;

    case 129:
        if (m3gCheckSectionDataLength(loader, data, 4) == M3G_FALSE) return M3G_FALSE;
        indicesCount = m3gLoadInt(data);
        data += 4;
        if (m3gCheckSectionDataLength(loader, data, indicesCount) == M3G_FALSE) return M3G_FALSE;
        indices = m3gAllocZ(M3G_INTERFACE(loader), sizeof(M3Gint) * indicesCount);
        if (!indices) return M3G_FALSE;
        for (i = 0; i < indicesCount; i++ ) {
            indices[i] = *data++;
        }
        break;

    case 130:
        if (m3gCheckSectionDataLength(loader, data, 4) == M3G_FALSE) return M3G_FALSE;
        indicesCount = m3gLoadInt(data);
        data += 4;

        /* Overflow? */
        if (indicesCount >= 0x40000000) {
            return M3G_FALSE;
        }

        if (m3gCheckSectionDataLength(loader, data, indicesCount * 2) == M3G_FALSE) return M3G_FALSE;
        indices = m3gAllocZ(M3G_INTERFACE(loader), sizeof(M3Gint) * indicesCount);
        if (!indices) return M3G_FALSE;
        for (i = 0; i < indicesCount; i++) {
            indices[i] = (M3Gushort)m3gLoadShort(data);
            data += 2;
        }
        break;

    default:
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, 4) == M3G_FALSE) goto cleanup;
    lengthCount = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (lengthCount >= 0x20000000) {
        goto cleanup;
    }

    if (m3gCheckSectionDataLength(loader, data, lengthCount * 4) == M3G_FALSE) goto cleanup;
    lengths = m3gAllocZ(M3G_INTERFACE(loader), sizeof(M3Gint) * lengthCount);
    if (!lengths) goto cleanup;

    for (i = 0; i < lengthCount; i++) {
        lengths[i] = m3gLoadInt(data);
        data += 4;
    }

    if (encoding == 0 || encoding == 1 || encoding == 2) {
        obj = m3gCreateImplicitStripBuffer( M3G_INTERFACE(loader),
                                            lengthCount,
                                            lengths,
                                            startIndex);
    }
    else {
        obj = m3gCreateStripBuffer( M3G_INTERFACE(loader),
                                    M3G_TRIANGLE_STRIPS,
                                    lengthCount,
                                    lengths,
                                    M3G_INT,
                                    indicesCount,
                                    indices);
    }

cleanup:
    m3gFree(M3G_INTERFACE(loader), indices);
    m3gFree(M3G_INTERFACE(loader), lengths);

    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    m3gEndObject(loader);
    m3gRewindObject(loader);
    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }
    m3gSkipObject(loader);

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a compositing mode
 */
static M3Gbool m3gLoadCompositingMode(Loader *loader)
{
    M3Gfloat f1, f2;
    M3Gubyte *data;
    M3GCompositingMode obj = m3gCreateCompositingMode(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 14);
    if (data == NULL) return M3G_FALSE;

    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gEnableDepthTest      (obj, *data++);
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gEnableDepthWrite     (obj, *data++);
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gEnableColorWrite     (obj, *data++);
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gSetAlphaWriteEnable  (obj, *data++);
    m3gSetBlending          (obj, *data++);
    {
        unsigned a = *data++; 
        m3gSetAlphaThreshold(obj, m3gDivif(a, 255));
    }
    if (!m3gLoadFloat(data, &f1) || !m3gLoadFloat(data + 4, &f2)) return M3G_FALSE;
    m3gSetDepthOffset       (obj, f1, f2);
    data += 8;

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a polygon mode
 */
static M3Gbool m3gLoadPolygonMode(Loader *loader)
{
    M3Gubyte *data;
    M3GPolygonMode obj = m3gCreatePolygonMode(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 6);
    if (data == NULL) return M3G_FALSE;

    m3gSetCulling                       (obj, *data++);
    m3gSetShading                       (obj, *data++);
    m3gSetWinding                       (obj, *data++);
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gSetTwoSidedLightingEnable        (obj, *data++);
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gSetLocalCameraLightingEnable     (obj, *data++);
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gSetPerspectiveCorrectionEnable   (obj, *data++);

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads an appearance
 */
static M3Gbool m3gLoadAppearance(Loader *loader)
{
    M3Guint textures, i;
    M3Gubyte *data;
    M3GAppearance obj = m3gCreateAppearance(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 21);
    if (data == NULL) return M3G_FALSE;

    m3gSetLayer(obj, (M3Gbyte)*data++);
    m3gSetCompositingMode(obj, (M3GCompositingMode)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_COMPOSITING_MODE));
    data += 4;
    m3gSetFog(obj, (M3GFog)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_FOG));
    data += 4;
    m3gSetPolygonMode(obj, (M3GPolygonMode)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_POLYGON_MODE));
    data += 4;
    m3gSetMaterial(obj, (M3GMaterial)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_MATERIAL));
    data += 4;

    textures = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (textures >= 0x20000000) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, textures * 4) == M3G_FALSE) return M3G_FALSE;
    for (i = 0; i < textures; i++) {
        M3GTexture tex = (M3GTexture)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_TEXTURE);
        if (!tex) {
            return M3G_FALSE;
        }
        m3gSetTexture(obj, i, tex);
        data += 4;
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a mesh
 */
static M3Gbool m3gLoadMesh(Loader *loader)
{
    M3Guint subMeshCount, i;
    M3GVertexBuffer vb;
    M3GIndexBuffer *ib = NULL;
    M3GAppearance *ap = NULL;
    M3Gubyte *data;
    M3GMesh obj = NULL;

    m3gBeginObject(loader);
    if (!m3gSkipNodeData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 8);
    if (data == NULL) return M3G_FALSE;

    vb = (M3GVertexBuffer)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_VERTEX_BUFFER);
    if (vb == NULL) return M3G_FALSE;
    data += 4;
    subMeshCount = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (subMeshCount >= 0x10000000) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, subMeshCount * 8) == M3G_FALSE) return M3G_FALSE;
    ib = m3gAllocZ(M3G_INTERFACE(loader), sizeof(*ib) * subMeshCount);
    ap = m3gAllocZ(M3G_INTERFACE(loader), sizeof(*ap) * subMeshCount);
    if (!ib || !ap) goto cleanup;

    for (i = 0; i < subMeshCount; i++) {
        M3GIndexBuffer indexBuffer;
        indexBuffer = (M3GIndexBuffer)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_INDEX_BUFFER);
        if (indexBuffer != NULL && loader->triConstraint != 0) {
            loader->triCount += indexBuffer->indexCount;
            if (loader->triCount > loader->triConstraint) goto cleanup;
        }
        ib[i] = indexBuffer;
        data += 4;
        ap[i] = (M3GAppearance)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_APPEARANCE);
        data += 4;
    }

    obj = m3gCreateMesh(    M3G_INTERFACE(loader),
                            vb,
                            ib,
                            ap,
                            subMeshCount);

cleanup:
    m3gFree(M3G_INTERFACE(loader), ib);
    m3gFree(M3G_INTERFACE(loader), ap);
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    m3gEndObject(loader);
    m3gRewindObject(loader);
    if (!m3gLoadNodeData(loader, (M3GNode)obj)) {
        return M3G_FALSE;
    }
    m3gSkipObject(loader);

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads group data
 */
static M3Gbool m3gLoadGroupData(Loader *loader, M3GGroup obj)
{
    M3Guint childCount, i;
    M3Gubyte *data;

    if (!m3gLoadNodeData(loader, (M3GNode)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 4);
    if (data == NULL) return M3G_FALSE;

    childCount = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (childCount >= 0x20000000) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, childCount * 4) == M3G_FALSE) return M3G_FALSE;
    for (i = 0; i < childCount; i++) {
        m3gAddChild(obj, (M3GNode)m3gGetLoaded(loader, m3gLoadInt(data), ANY_NODE_CLASS));
        data += 4;
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a group
 */
static M3Gbool m3gLoadGroup(Loader *loader)
{
    M3GGroup obj = m3gCreateGroup(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadGroupData(loader, obj)) {
        return M3G_FALSE;
    }

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a world
 */
static M3Gbool m3gLoadWorld(Loader *loader)
{
    M3GCamera cam;
    M3Gubyte *data;
    M3GWorld obj = m3gCreateWorld(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadGroupData(loader, (M3GGroup)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 8);
    if (data == NULL) return M3G_FALSE;

    cam = (M3GCamera)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_CAMERA);
    data += 4;
    if (cam != NULL) {
        m3gSetActiveCamera(obj, cam);
    }
    m3gSetBackground(obj, (M3GBackground)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_BACKGROUND));
    data += 4;

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a light
 */
static M3Gbool m3gLoadLight(Loader *loader)
{
    M3Gfloat f1, f2, f3;
    M3Gubyte *data;
    M3GLight obj = m3gCreateLight(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadNodeData(loader, (M3GNode)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 28);
    if (data == NULL) return M3G_FALSE;

    if (!m3gLoadFloat(data, &f1) ||
        !m3gLoadFloat(data + 4, &f2) ||
        !m3gLoadFloat(data + 8, &f3)) return M3G_FALSE;
    m3gSetAttenuation   (obj, f1, f2, f3);
    data += 12;
    m3gSetLightColor    (obj, m3gLoadRGB(data));
    data += 3;
    m3gSetLightMode     (obj, *data++);
    if (!m3gLoadFloat(data, &f1)) return M3G_FALSE;
    m3gSetIntensity     (obj, f1);
    data += 4;
    if (!m3gLoadFloat(data, &f1)) return M3G_FALSE;
    m3gSetSpotAngle     (obj, f1);
    data += 4;
    if (!m3gLoadFloat(data, &f1)) return M3G_FALSE;
    m3gSetSpotExponent  (obj, f1);
    data += 4;

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a keyframe sequence
 */
static M3Gbool m3gLoadKeyframeSequence(Loader *loader)
{
    M3Guint i, j, interpolation, repeatMode, encoding, duration,
            rangeFirst, rangeLast, components, keyFrames, size;
    M3Gfloat *values;
    M3Gubyte *data;
    M3GKeyframeSequence obj;

    m3gBeginObject(loader);
    if (!m3gSkipObject3DData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 23);
    if (data == NULL) return M3G_FALSE;

    interpolation = *data++;
    repeatMode    = *data++;
    encoding      = *data++;
    duration      = m3gLoadInt(data);
    data += 4;
    rangeFirst    = m3gLoadInt(data);
    data += 4;
    rangeLast     = m3gLoadInt(data);
    data += 4;
    components    = m3gLoadInt(data);
    data += 4;
    keyFrames     = m3gLoadInt(data);
    data += 4;

    if (encoding == 0) {
        size = keyFrames * (4 + components * 4);
    }
    else {
        size = components * 8 + keyFrames * (4 + components * (encoding == 1 ? 1 : 2));
    }

    /* Overflow? */
    if (size < keyFrames) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, size) == M3G_FALSE) return M3G_FALSE;

    obj = m3gCreateKeyframeSequence(M3G_INTERFACE(loader), keyFrames, components, interpolation);
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    m3gSetRepeatMode(obj, repeatMode);
    m3gSetDuration(obj, duration);
    m3gSetValidRange(obj, rangeFirst, rangeLast);

    values = m3gAllocZ(M3G_INTERFACE(loader), sizeof(M3Gfloat) * components);
    if (!values) return M3G_FALSE;

    if (encoding == 0) {
        for (i = 0; i < keyFrames; i++ ) {
            M3Gint time = m3gLoadInt(data);
            data += 4;

            for (j = 0; j < components; j++ ) {
                if (!m3gLoadFloat(data, &values[j])) {
                    m3gFree(M3G_INTERFACE(loader), values);
                    return M3G_FALSE;
                }
                data += 4;
            }

            m3gSetKeyframe(obj, i, time, components, values);
        }
    }
    else {
        M3Gfloat *vectorBiasScale = m3gAllocZ(M3G_INTERFACE(loader), sizeof(M3Gfloat) * components * 2);
        if (!vectorBiasScale) {
            m3gFree(M3G_INTERFACE(loader), values);
            return M3G_FALSE;
        }

        for (i = 0; i < components; i++ ) {
            if (!m3gLoadFloat(data, &vectorBiasScale[i])) {
                m3gFree(M3G_INTERFACE(loader), vectorBiasScale);
                m3gFree(M3G_INTERFACE(loader), values);
                return M3G_FALSE;
            }
            data += 4;
        }
        for (i = 0; i < components; i++ ) {
            if (!m3gLoadFloat(data, &vectorBiasScale[i + components])) {
                m3gFree(M3G_INTERFACE(loader), vectorBiasScale);
                m3gFree(M3G_INTERFACE(loader), values);
                return M3G_FALSE;
            }
            data += 4;
        }

        for (i = 0; i < keyFrames; i++ ) {
            M3Gint time;
            time = m3gLoadInt(data);
            data += 4;

            if (encoding == 1) {
                for (j = 0; j < components; j++ ) {
                    M3Gubyte v = *data++;
                    values[j] = vectorBiasScale[j] + ((vectorBiasScale[j + components] * v ) / 255.0f);
                }
            }
            else {
                for (j = 0; j < components; j++ ) {
                    M3Gushort v = m3gLoadShort(data);
                    data += 2;
                    values[j] = vectorBiasScale[j] + ((vectorBiasScale[j + components] * v) / 65535.0f);
                }
            }

            m3gSetKeyframe(obj, i, time, components, values);
        }

        m3gFree(M3G_INTERFACE(loader), vectorBiasScale);
    }

    m3gFree(M3G_INTERFACE(loader), values);

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    m3gEndObject(loader);
    m3gRewindObject(loader);
    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }
    m3gSkipObject(loader);

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads an animation controller
 */
static M3Gbool m3gLoadAnimationController(Loader *loader)
{
    M3Gfloat speed, weight, referenceSeqTime;
    M3Gint referenceWorldTime;
    M3Gubyte *data;
    M3GAnimationController obj = m3gCreateAnimationController(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 24);
    if (data == NULL) return M3G_FALSE;

    if (!m3gLoadFloat(data, &speed)) return M3G_FALSE;
    data += 4;
    if (!m3gLoadFloat(data, &weight)) return M3G_FALSE;
    data += 4;
    m3gSetActiveInterval(obj, m3gLoadInt(data), m3gLoadInt(data + 4));
    data += 8;
    if (!m3gLoadFloat(data, &referenceSeqTime)) return M3G_FALSE;
    data += 4;
    referenceWorldTime = m3gLoadInt(data);
    data += 4;

    m3gSetPosition(obj, referenceSeqTime, referenceWorldTime);
    m3gSetSpeed(obj, speed, referenceWorldTime);
    m3gSetWeight(obj, weight);

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads an animation track
 */
static M3Gbool m3gLoadAnimationTrack(Loader *loader)
{
    M3Gint property;
    M3GKeyframeSequence ks;
    M3GAnimationController ac;
    M3Gubyte *data;
    M3GAnimationTrack obj;

    m3gBeginObject(loader);
    if (!m3gSkipObject3DData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 12);
    if (data == NULL) return M3G_FALSE;

    ks = (M3GKeyframeSequence)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_KEYFRAME_SEQUENCE);
    data += 4;
    ac = (M3GAnimationController)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_ANIMATION_CONTROLLER);
    data += 4;
    property = m3gLoadInt(data);
    data += 4;

    obj = m3gCreateAnimationTrack(M3G_INTERFACE(loader), ks, property);
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    m3gSetController(obj, ac);

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    m3gEndObject(loader);
    m3gRewindObject(loader);
    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }
    m3gSkipObject(loader);

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a material
 */
static M3Gbool m3gLoadMaterial(Loader *loader)
{
    M3Gfloat f1;
    M3Gubyte *data;
    M3GMaterial obj = m3gCreateMaterial(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 18);
    if (data == NULL) return M3G_FALSE;

    m3gSetColor(obj, M3G_AMBIENT_BIT, m3gLoadRGB(data));
    data += 3;
    m3gSetColor(obj, M3G_DIFFUSE_BIT, m3gLoadARGB(data));
    data += 4;
    m3gSetColor(obj, M3G_EMISSIVE_BIT, m3gLoadRGB(data));
    data += 3;
    m3gSetColor(obj, M3G_SPECULAR_BIT, m3gLoadRGB(data));
    data += 3;
    if (!m3gLoadFloat(data, &f1)) return M3G_FALSE;
    m3gSetShininess(obj, f1);
    data += 4;
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    m3gSetVertexColorTrackingEnable(obj, *data++);

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a fog
 */
static M3Gbool m3gLoadFog(Loader *loader)
{
    M3Gfloat f1, f2;
    M3Gubyte *data;
    M3GFog obj = m3gCreateFog(M3G_INTERFACE(loader));
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }

    data = m3gGetSectionDataPtr(loader, 4);
    if (data == NULL) return M3G_FALSE;

    m3gSetFogColor(obj, m3gLoadRGB(data));
    data += 3;
    m3gSetFogMode(obj, *data);

    if (*data++ == M3G_EXPONENTIAL_FOG) {
        if (m3gCheckSectionDataLength(loader, data, 4) == M3G_FALSE) return M3G_FALSE;
        if (!m3gLoadFloat(data, &f1)) return M3G_FALSE;
        m3gSetFogDensity(obj, f1);
        data += 4;
    }
    else {
        if (m3gCheckSectionDataLength(loader, data, 8) == M3G_FALSE) return M3G_FALSE;
        if (!m3gLoadFloat(data, &f1) || !m3gLoadFloat(data + 4, &f2)) return M3G_FALSE;
        m3gSetFogLinear(obj, f1, f2);
        data += 8;
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads an image
 */
static M3Gbool m3gLoadImage(Loader *loader)
{
    M3GImageFormat format;
    M3Guint width, height;
    M3Gbyte isMutable;
    M3Gubyte *data;
    M3GImage obj;

    m3gBeginObject(loader);
    if (!m3gSkipObject3DData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 10);
    if (data == NULL) return M3G_FALSE;

    format = (M3GImageFormat)*data++;
    if (!m3gVerifyBool(data)) return M3G_FALSE;
    isMutable = *data++;
    width = m3gLoadInt(data);
    data += 4;
    height = m3gLoadInt(data);
    data += 4;

    if (isMutable) {
        obj = m3gCreateImage(M3G_INTERFACE(loader), format,
                                                    width, height,
                                                    M3G_RENDERING_TARGET);
    }
    else {
        M3Gubyte *palette = NULL, *pixels = NULL;
        M3Gint paletteLength, pixelsLength, bpp;

        switch(format) {
        case M3G_ALPHA:             bpp = 1; break;
        case M3G_LUMINANCE:         bpp = 1; break;
        case M3G_LUMINANCE_ALPHA:   bpp = 2; break;
        case M3G_RGB:               bpp = 3; break;
        case M3G_RGBA:              bpp = 4; break;
        default:                    return M3G_FALSE;
        }

        if (m3gCheckSectionDataLength(loader, data, 4) == M3G_FALSE) return M3G_FALSE;
        paletteLength = m3gLoadInt(data);
        data += 4;

        if (paletteLength > 0) {
            if (m3gCheckSectionDataLength(loader, data, paletteLength) == M3G_FALSE) return M3G_FALSE;
            palette = data;
            data += paletteLength;
        }

        if (m3gCheckSectionDataLength(loader, data, 4) == M3G_FALSE) return M3G_FALSE;
        pixelsLength = m3gLoadInt(data);
        data += 4;
        if (m3gCheckSectionDataLength(loader, data, pixelsLength) == M3G_FALSE) return M3G_FALSE;
        pixels = data;
        data += pixelsLength;

        if (palette != NULL) {
            obj = m3gCreateImage(M3G_INTERFACE(loader), format,
                                                        width, height,
                                                        M3G_PALETTED);
            if (obj != NULL) {
                M3Gint numEntries = paletteLength / bpp;
                if (numEntries > 256) {
                    numEntries = 256;
                }
                m3gSetImage(obj, pixels);
                m3gSetImagePalette(obj, numEntries, palette);
                m3gCommitImage(obj);
            }
        }
        else {
            obj = m3gCreateImage(M3G_INTERFACE(loader), format,
                                                        width, height,
                                                        0);
            if (obj != NULL) {
                m3gSetImage(obj, pixels);
                m3gCommitImage(obj);
            }
        }
    }

    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    m3gEndObject(loader);
    m3gRewindObject(loader);
    if (!m3gLoadObject3DData(loader, (M3GObject)obj)) {
        return M3G_FALSE;
    }
    m3gSkipObject(loader);

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a texture
 */
static M3Gbool m3gLoadTexture(Loader *loader)
{
    M3GImage image;
    M3Gubyte *data;
    M3GTexture obj;

    m3gBeginObject(loader);
    if (!m3gSkipTransformableData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 12);
    if (data == NULL) return M3G_FALSE;

    image = (M3GImage)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_IMAGE);
    data += 4;

    obj = m3gCreateTexture(M3G_INTERFACE(loader), image);
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    m3gSetBlendColor(obj, m3gLoadRGB(data));
    data += 3;
    m3gTextureSetBlending(obj, *data++);
    m3gSetWrapping(obj, data[0], data[1]);
    data += 2;
    m3gSetFiltering(obj, data[0], data[1]);
    data += 2;

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    m3gEndObject(loader);
    m3gRewindObject(loader);
    if (!m3gLoadTransformableData(loader, (M3GTransformable)obj)) {
        return M3G_FALSE;
    }
    m3gSkipObject(loader);

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a skinned mesh
 */
static M3Gbool m3gLoadSkinnedMesh(Loader *loader)
{
    M3GVertexBuffer vb;
    M3Guint i, subMeshCount, transformReferenceCount, firstVertex, vertexCount;
    M3Gint weight;
    M3GIndexBuffer *ib = NULL;
    M3GAppearance *ap = NULL;
    M3GGroup skeleton;
    M3GNode bone;
    M3Gubyte *data;
    M3GSkinnedMesh obj = NULL;

    m3gBeginObject(loader);
    if (!m3gSkipNodeData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 8);
    if (data == NULL) return M3G_FALSE;

    vb = (M3GVertexBuffer)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_VERTEX_BUFFER);
    if (vb == NULL) return M3G_FALSE;
    data += 4;
    subMeshCount = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (subMeshCount >= 0x10000000) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, subMeshCount * 8) == M3G_FALSE) return M3G_FALSE;
    ib = m3gAllocZ(M3G_INTERFACE(loader), sizeof(*ib) * subMeshCount);
    ap = m3gAllocZ(M3G_INTERFACE(loader), sizeof(*ap) * subMeshCount);
    if (!ib || !ap) goto cleanup;

    for (i = 0; i < subMeshCount; i++) {
        ib[i] = (M3GIndexBuffer)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_INDEX_BUFFER);
        data += 4;
        ap[i] = (M3GAppearance)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_APPEARANCE);
        data += 4;
    }

    if (m3gCheckSectionDataLength(loader, data, 8) == M3G_FALSE) goto cleanup;
    skeleton = (M3GGroup)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_GROUP);
    data += 4;

    obj = m3gCreateSkinnedMesh( M3G_INTERFACE(loader),
                                vb,
                                ib,
                                ap,
                                subMeshCount,
                                skeleton);

cleanup:
    m3gFree(M3G_INTERFACE(loader), ib);
    m3gFree(M3G_INTERFACE(loader), ap);
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    transformReferenceCount = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (transformReferenceCount >= 0x08000000) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, transformReferenceCount * 16) == M3G_FALSE) return M3G_FALSE;
    for (i = 0; i < transformReferenceCount; i++) {
        bone        = (M3GNode)m3gGetLoaded(loader, m3gLoadInt(data), ANY_NODE_CLASS);
        data += 4;
        firstVertex = m3gLoadInt(data);
        data += 4;
        vertexCount = m3gLoadInt(data);
        data += 4;
        weight      = m3gLoadInt(data);
        data += 4;
        m3gAddTransform(obj, bone, weight, firstVertex, vertexCount);
    }

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    m3gEndObject(loader);
    m3gRewindObject(loader);
    if (!m3gLoadNodeData(loader, (M3GNode)obj)) {
        return M3G_FALSE;
    }
    m3gSkipObject(loader);

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a morphing mesh
 */
static M3Gbool m3gLoadMorphingMesh(Loader *loader)
{
    M3GVertexBuffer vb, *targets = NULL;
    M3Guint i, subMeshCount, targetCount;
    M3Gfloat *weights = NULL;
    M3GIndexBuffer *ib = NULL;
    M3GAppearance *ap = NULL;
    M3Gubyte *data;
    M3GMorphingMesh obj = NULL;

    m3gBeginObject(loader);
    if (!m3gSkipNodeData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 8);
    if (data == NULL) return M3G_FALSE;

    vb = (M3GVertexBuffer)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_VERTEX_BUFFER);
    if (vb == NULL) return M3G_FALSE;
    data += 4;
    subMeshCount = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (subMeshCount >= 0x10000000) {
        return M3G_FALSE;
    }

    if (m3gCheckSectionDataLength(loader, data, subMeshCount * 8) == M3G_FALSE) return M3G_FALSE;
    ib = m3gAllocZ(M3G_INTERFACE(loader), sizeof(*ib) * subMeshCount);
    ap = m3gAllocZ(M3G_INTERFACE(loader), sizeof(*ap) * subMeshCount);
    if (!ib || !ap) goto cleanup;

    for (i = 0; i < subMeshCount; i++) {
        ib[i] = (M3GIndexBuffer)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_INDEX_BUFFER);
        data += 4;
        ap[i] = (M3GAppearance)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_APPEARANCE);
        data += 4;
    }

    if (m3gCheckSectionDataLength(loader, data, 4) == M3G_FALSE) goto cleanup;
    targetCount = m3gLoadInt(data);
    data += 4;

    /* Overflow? */
    if (targetCount >= 0x10000000) {
        goto cleanup;
    }

    if (m3gCheckSectionDataLength(loader, data, targetCount * 8) == M3G_FALSE) goto cleanup;
    weights = m3gAllocZ(M3G_INTERFACE(loader), sizeof(M3Gfloat) * targetCount);
    targets = m3gAllocZ(M3G_INTERFACE(loader), sizeof(*targets) * targetCount);
    if (!weights || !targets) goto cleanup;

    for (i = 0; i < targetCount; i++) {
        targets[i] = (M3GVertexBuffer)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_VERTEX_BUFFER);
        data += 4;
        if (!m3gLoadFloat(data, &weights[i])) goto cleanup;
        data += 4;
    }

    obj = m3gCreateMorphingMesh(    M3G_INTERFACE(loader),
                                    vb,
                                    targets,
                                    ib,
                                    ap,
                                    subMeshCount,
                                    targetCount);

cleanup:
    m3gFree(M3G_INTERFACE(loader), ib);
    m3gFree(M3G_INTERFACE(loader), ap);
    m3gFree(M3G_INTERFACE(loader), targets);
    m3gFree(M3G_INTERFACE(loader), weights);
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    m3gEndObject(loader);
    m3gRewindObject(loader);
    if (!m3gLoadNodeData(loader, (M3GNode)obj)) {
        return M3G_FALSE;
    }
    m3gSkipObject(loader);

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a sprite
 */
static M3Gbool m3gLoadSprite(Loader *loader)
{
    M3GImage image;
    M3GAppearance appearance;
    M3Gubyte *data;
    M3GSprite obj;

    m3gBeginObject(loader);
    if (!m3gSkipNodeData(loader)) return M3G_FALSE;

    data = m3gGetSectionDataPtr(loader, 25);
    if (data == NULL) return M3G_FALSE;

    image = (M3GImage)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_IMAGE);
    data += 4;
    appearance = (M3GAppearance)m3gGetLoaded(loader, m3gLoadInt(data), M3G_CLASS_APPEARANCE);
    data += 4;

    if (!m3gVerifyBool(data)) return M3G_FALSE;
    obj = m3gCreateSprite(  M3G_INTERFACE(loader),
                            *data++,
                            image,
                            appearance);
    loader->loadedObject = (M3GObject)obj;
    if (!obj) return M3G_FALSE;

    m3gSetCrop(obj, m3gLoadInt(data),
                    m3gLoadInt(data + 4),
                    m3gLoadInt(data + 8),
                    m3gLoadInt(data + 12));
    data += 16;

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    m3gEndObject(loader);
    m3gRewindObject(loader);
    if (!m3gLoadNodeData(loader, (M3GNode)obj)) {
        return M3G_FALSE;
    }
    m3gSkipObject(loader);

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads a M3G file header
 */
static M3Gbool m3gLoadHeader(Loader *loader)
{
    M3Gubyte *data;
    data = m3gGetSectionDataPtr(loader, 12);
    if (data == NULL) return M3G_FALSE;

    /* Check version */
    if (data[0] != 1 || data[1] != 0 || loader->sectionNum != 0) {
        return M3G_FALSE;
    }
    data += 2;

    if (!m3gVerifyBool(data)) return M3G_FALSE;
    loader->hasReferences = *data++;
    loader->fileSize = m3gLoadInt(data);
    data += 4;
    loader->contentSize = m3gLoadInt(data);
    data += 4;

    /* Skip authoring field */
    while(*data++)
        if (m3gCheckSectionDataLength(loader, data, 1) == M3G_FALSE) return M3G_FALSE;

    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));

    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Skips external reference
 */
static M3Gbool m3gLoadExternalReference(Loader *loader)
{
    M3Gubyte *data;
    if (loader->sectionNum != 1 || !loader->hasReferences)
        return M3G_FALSE;
    data = m3gGetSectionDataPtr(loader, 1);
    while(*data++) { /* Skip string */
        if (m3gCheckSectionDataLength(loader, data, 1) == M3G_FALSE)
            return M3G_FALSE;
    }
    m3gAdvanceSectionData(loader, data - m3gGetSectionDataPtr(loader, 0));
    return M3G_TRUE;
}

/*!
 * \internal
 * \brief Loads an object
 */
static LoaderState m3gLoadObject(Loader *loader)
{
    M3Gubyte *data = m3gGetSectionDataPtr(loader, loader->sectionBytesRequired);

    if (data == NULL) {
        loader->localState = LOADSTATE_CHECKSUM;
        return LOADSTATE_SECTION;
    }

    m3gAdvanceSectionData(loader, loader->sectionBytesRequired);

    if (loader->localState == LOADSTATE_ENTER) {
        M3Gbool status = M3G_TRUE;

        loader->objectType = *data++;
        loader->loadedObject = NULL;
        loader->localState = LOADSTATE_EXIT;
        loader->sectionBytesRequired = m3gLoadInt(data);
        data += 4;
        if (loader->sectionNum == 0 && loader->objectType != 0) {
            m3gRaiseError(M3G_INTERFACE(loader), M3G_IO_ERROR);
            return LOADSTATE_ERROR;
        }

        switch(loader->objectType) {
        case 0: /* Header Object */
            status = m3gLoadHeader(loader);
            break;
        case 1: /* AnimationController */
            status = m3gLoadAnimationController(loader);
            break;
        case 2: /* AnimationTrack */
            status = m3gLoadAnimationTrack(loader);
            break;
        case 3: /* Appearance */
            status = m3gLoadAppearance(loader);
            break;
        case 4: /* Background */
            status = m3gLoadBackground(loader);
            break;
        case 5: /* Camera */
            status = m3gLoadCamera(loader);
            break;
        case 6: /* CompositingMode */
            status = m3gLoadCompositingMode(loader);
            break;
        case 7: /* Fog */
            status = m3gLoadFog(loader);
            break;
        case 8: /* PolygonMode */
            status = m3gLoadPolygonMode(loader);
            break;
        case 9: /* Group */
            status = m3gLoadGroup(loader);
            break;
        case 10: /* Image2D */
            status = m3gLoadImage(loader);
            break;
        case 11: /* TriangleStripArray */
            status = m3gLoadTsa(loader);
            break;
        case 12: /* Light */
            status = m3gLoadLight(loader);
            break;
        case 13: /* Material */
            status = m3gLoadMaterial(loader);
            break;
        case 14: /* Mesh */
            status = m3gLoadMesh(loader);
            break;
        case 15: /* MorphingMesh */
            status = m3gLoadMorphingMesh(loader);
            break;
        case 16: /* SkinnedMesh */
            status = m3gLoadSkinnedMesh(loader);
            break;
        case 17: /* Texture2D */
            status = m3gLoadTexture(loader);
            break;
        case 18: /* Sprite */
            status = m3gLoadSprite(loader);
            break;
        case 19: /* KeyframeSequence */
            status = m3gLoadKeyframeSequence(loader);
            break;
        case 20: /* VertexArray */
            status = m3gLoadVertexArray(loader);
            break;
        case 21: /* VertexBuffer */
            status = m3gLoadVertexBuffer(loader);
            break;
        case 22: /* World */
            status = m3gLoadWorld(loader);
            break;
        case 255: /* External Reference */
            status = m3gLoadExternalReference(loader);
            break;
        default:  /* 23 ... 254 Reserved for use in future versions of the file format */
            status = M3G_FALSE;
            break;
        }

        /* Check if object loading caused an error */
        if (m3gErrorRaised(M3G_INTERFACE(loader))) {
            m3gDeleteObject(loader->loadedObject);
            return LOADSTATE_ERROR;
        }
        if (!status || 
            m3gGetSectionDataPtr(loader, 0) != data + loader->sectionBytesRequired) {
            m3gDeleteObject(loader->loadedObject);
            m3gRaiseError(M3G_INTERFACE(loader), M3G_IO_ERROR);
            return LOADSTATE_ERROR;
        }
        loader->sectionBytesRequired = 0;
        
        /* Add object to loaded objects array */
        if (loader->loadedObject != NULL) {
            if (m3gArrayAppend(&loader->refArray, loader->loadedObject, M3G_INTERFACE(loader)) == -1) {
                /* OOM */
                m3gDeleteObject(loader->loadedObject);
                return LOADSTATE_ERROR;
            }
            m3gAddRef(loader->loadedObject);
        }
    }
    else {
        loader->sectionBytesRequired = M3G_MIN_OBJECT_SIZE;
        loader->localState = LOADSTATE_ENTER;
    }

    return LOADSTATE_OBJECT;
}

/*!
 * \internal
 * \brief Handles branching to different subroutines upon re-entry
 *
 * When sufficient data is available in internal buffers, this
 * function gets called and directs execution into the coroutine
 * matching the current global state.
 */
static LoaderState m3gLoaderMain(Loader *loader)
{
    M3G_VALIDATE_OBJECT(loader);
    M3G_ASSERT(loader->bytesRequired > 0);
    M3G_ASSERT(loader->bytesRequired <= loader->stream.bytesAvailable);

    switch (loader->state) {
    case LOADSTATE_INITIAL:
        loader->stream.totalBytes = 0;
        loader->localState = LOADSTATE_ENTER;
        loader->fileSize = 0x00ffffff;
        loader->bytesRequired = sizeof(PNG_FILE_IDENTIFIER);
        return LOADSTATE_IDENTIFIER;
    case LOADSTATE_IDENTIFIER:
        return m3gLoadIdentifier(loader);
    case LOADSTATE_SECTION:
        return m3gLoadSection(loader);
    case LOADSTATE_OBJECT:
        return m3gLoadObject(loader);
    default:
        loader->bytesRequired = 0;
        return LOADSTATE_ERROR;
    }
}

/*!
 * \brief Deletes all unreferenced objects
 */
static void m3gCleanupLoader(M3GLoader loader)
{
    M3Gint i, j, n;
    PointerArray *refs;
    M3Gbool referenced;
    M3GObject obj;    

    refs = &loader->refArray;
    n = m3gArraySize(refs);

    /* All unreferenced objects will be deleted, as their ref count becomes 0 */
    for (i = 0; i < n; ++i) {
        obj = m3gGetLoadedPtr(refs, i, &referenced);
        m3gDeleteRef(obj);
    }
    m3gClearArray(&loader->refArray);

    n = m3gArraySize(&loader->userDataArray);
    for (i = 0; i < n; ++i)
    {
        UserData *data = (UserData *)m3gGetArrayElement(&loader->userDataArray, i);
        for (j = 0; j < data->numParams; ++j)
            m3gFree(M3G_INTERFACE(loader), data->params[j]);
        m3gFree(M3G_INTERFACE(loader), data->params);
        m3gFree(M3G_INTERFACE(loader), data->paramLengths);
        m3gFree(M3G_INTERFACE(loader), data->paramId);
        m3gFree(M3G_INTERFACE(loader), data);
    }
    m3gClearArray(&loader->userDataArray);

    m3gFree(M3G_INTERFACE(loader), loader->allocatedSectionData);
    loader->allocatedSectionData = NULL;
}

/*!
 * \internal
 * \brief Resets the loader
 */
static void m3gResetLoader(Loader *loader)
{
    /* Reset loader state */
    loader->state = LOADSTATE_INITIAL;
    loader->bytesRequired = sizeof(PNG_FILE_IDENTIFIER);

    m3gCleanupLoader(loader);
    m3gResetBufferedData(&loader->stream);
}

/*----------------------------------------------------------------------
 * Virtual function table
 *--------------------------------------------------------------------*/

static const ObjectVFTable m3gvf_Loader = {
    NULL, /* ApplyAnimation */
    NULL, /* IsCompatible */
    NULL, /* UpdateProperty */
    NULL, /* DoGetReferences */
    NULL, /* FindID */
    NULL, /* Duplicate */
    m3gDestroyLoader
};
    

/*----------------------------------------------------------------------
 * Public interface
 *--------------------------------------------------------------------*/

/*!
 * \brief Creates a new loader instance
 */
M3G_API M3GLoader m3gCreateLoader(M3GInterface m3g)
{
    Loader *loader;
    M3G_VALIDATE_INTERFACE(m3g);

    loader = m3gAllocZ(m3g, sizeof(*loader));
    if (loader != NULL) {
        m3gInitObject(&loader->object, m3g, M3G_CLASS_LOADER);
        m3gInitArray(&loader->refArray);
        m3gInitArray(&loader->userDataArray);
        loader->bytesRequired = sizeof(PNG_FILE_IDENTIFIER);
        loader->state = LOADSTATE_INITIAL;
        loader->sectionNum = -1;
    }
    return loader;
}

/*!
 * \brief Import a set of objects into the reference table of a loader
 *
 * This is intended for passing in external references, decoded in
 * Java
 */
M3G_API void m3gImportObjects(M3GLoader loader, M3Gint n, M3GObject *refs)
{
    int i;
    M3G_VALIDATE_OBJECT(loader);

    if (loader->state == LOADSTATE_DONE)
        m3gResetLoader(loader);
    for (i = 0; i < n; ++i) {
        /* For loop is interrupted in case of OOM */
        if (m3gArrayAppend(&loader->refArray,
                           refs[i],
                           M3G_INTERFACE(loader)) == -1) {
            break;
        }
        m3gAddRef(refs[i]);
    }
}

/*!
 * \brief Return the complete reference table for this loader instance
 *
 * The reference table will contain the handle of each object that has
 * been loaded so far.
 *
 * \param loader loader instance
 * \param buffer destination buffer, or NULL to just get
 *               the number of unreferenced objects
 * \return number of unreferenced objects
 */
M3G_API M3Gint m3gGetLoadedObjects(M3GLoader loader, M3GObject *buffer)
{
    PointerArray *refs;
    int i, n, unref = 0;
    M3Gbool referenced;
    M3GObject obj, *dst = buffer;
    M3G_VALIDATE_OBJECT(loader);

    /* If error in decoding, reset and return 0 objects */
    if (loader->state < LOADSTATE_INITIAL) {
        return 0;
    }

    refs = &loader->refArray;
    n = m3gArraySize(refs);

    /* Scan unreferenced objects */
    for (i = 0; i < n; ++i) {
        obj = m3gGetLoadedPtr(refs, i, &referenced);
        if (!referenced) {
            unref++;
            if (dst != NULL) {
                *dst++ = obj;
            }
        }
    }

    return unref;
}

/*!
 * \brief Submits data to the loader for processing
 *
 * Upon data input, the loader will either read more objects from the
 * stream or buffer the data for processing later on. The return value
 * will indicate how many bytes of data are required before the next
 * data element can be loaded, but that data can still be submitted in
 * smaller blocks.
 *
 * \param loader the loader instance
 * \param bytes  the number of bytes in the data
 * \param data   pointer to the data
 *
 * \return the number of bytes required to load the next data element,
 * or zero to indicate that loading has finished
 */

#ifdef M3G_ENABLE_PROFILING
static M3Gsizei m3gDecodeDataInternal(M3GLoader loader,
                               M3Gsizei bytes,
                               const M3Gubyte *data);

M3G_API M3Gsizei m3gDecodeData(M3GLoader loader,
                               M3Gsizei bytes,
                               const M3Gubyte *data)
{
    M3Gsizei bytesReq;
    M3G_BEGIN_PROFILE(M3G_INTERFACE(loader), M3G_PROFILE_LOADER_DECODE);
    bytesReq = m3gDecodeDataInternal(loader, bytes, data);
    M3G_END_PROFILE(M3G_INTERFACE(loader), M3G_PROFILE_LOADER_DECODE);
    return bytesReq;
}

static M3Gsizei m3gDecodeDataInternal(M3GLoader loader,
                               M3Gsizei bytes,
                               const M3Gubyte *data)

#else
M3G_API M3Gsizei m3gDecodeData(M3GLoader loader,
                               M3Gsizei bytes,
                               const M3Gubyte *data)

#endif
{
    m3gErrorHandler *errorHandler;
    Interface *m3g = M3G_INTERFACE(loader);

    M3G_VALIDATE_OBJECT(loader);
    M3G_VALIDATE_INTERFACE(m3g);

    /* Check for errors */
    if (bytes <= 0 || data == NULL) {
        m3gRaiseError(m3g, M3G_INVALID_VALUE);
        return 0;
    }

    if (loader->state == LOADSTATE_DONE)
        m3gResetLoader(loader);

    /* Submit data, then load until we run out of data again or are
     * finished */

    if (!m3gBufferData(m3g, &loader->stream, bytes, data)) {
        return 0;
    }

    /* Disable error handler */
    errorHandler = m3gSetErrorHandler(m3g, NULL);

    /* Continue loading if sufficient data has arrived */
    while (loader->bytesRequired > 0
           && loader->bytesRequired <= loader->stream.bytesAvailable) {
        loader->state = m3gLoaderMain(loader);
    }

    /* Restore error handler */
    m3gSetErrorHandler(m3g, errorHandler); 

    /* Check if error was raised */
    if (m3gErrorRaised(m3g) != M3G_NO_ERROR) {
        /* Need to free all loaded objects */
        m3gResetLoader(loader);

        /* Raise again with original error handler in place */
        if (m3gErrorRaised(m3g) == M3G_OUT_OF_MEMORY)
            m3gRaiseError(m3g, M3G_OUT_OF_MEMORY);
        else
            m3gRaiseError(m3g, M3G_IO_ERROR);
        return 0;
    }

    /* Return the number of bytes we need for loading to proceed
     * further; clamp to zero in case we're exiting due to an error,
     * or just have been fed excess data */
    {
        M3Gsizei bytesReq =
            loader->bytesRequired - loader->stream.bytesAvailable;

        /* Check if whole file is done */
        if (loader->stream.totalBytes >= loader->fileSize) {
            loader->state = LOADSTATE_DONE;
            bytesReq = 0;
        }

        return (bytesReq >= 0) ? bytesReq : 0;
    }
}

/*!
 * \brief Return all loaded objects with user parameters
 *
 * \param loader  the loader instance
 * \param objects an array for objects with user parameters,
 *                or null to return the number of objects
 *
 * \return Number of objects with user parameters
 */
M3G_API M3Gint m3gGetObjectsWithUserParameters(M3GLoader loader, M3GObject *objects) {
    const Loader *ldr = (const Loader *) loader;
    M3G_VALIDATE_OBJECT(ldr);
    {
        M3Gint i, n;
        n = m3gArraySize(&ldr->userDataArray);
    
        if (objects != NULL)
            for (i = 0; i < n; ++i)
            {
                const UserData *data = (const UserData *)m3gGetArrayElement(&ldr->userDataArray, i);
                objects[i] = data->object;
            }
        return n;
    }
}

/*!
 * \brief Return the number of user parameters loaded for an object
 */
M3G_API M3Gint m3gGetNumUserParameters(M3GLoader loader, M3Gint object)
{
    const Loader *ldr = (const Loader *) loader;
    M3G_VALIDATE_OBJECT(ldr);
    {
        const UserData *data = (const UserData *)m3gGetArrayElement(&ldr->userDataArray, object);
        return (data != NULL) ? data->numParams : 0;
    }
}

/*!
 * \brief Set constraints for loading.
 * \param triConstraint maximum triangle count
 */
M3G_API void m3gSetConstraints(M3GLoader loader, M3Gint triConstraint)
{
    M3G_VALIDATE_OBJECT(loader);
    loader->triConstraint = triConstraint;
}

/*!
 * \brief Return the given user parameter for an object
 *
 * \param loader the loader instance
 * \param object the object to query
 * \param index  index of the string to query
 * \param buffer buffer to copy the data into,
 *               or NULL to just query the length
 *
 * \return id of the parameter, or the length of the string if data was NULL
 */
M3G_API M3Gsizei m3gGetUserParameter(M3GLoader loader,
                                     M3Gint object,
                                     M3Gint index,
                                     M3Gbyte *buffer)
{
    const Loader *ldr = (const Loader *) loader;
    M3G_VALIDATE_OBJECT(ldr);
    {
        const UserData *data = (const UserData *)m3gGetArrayElement(&ldr->userDataArray, object);
        if (data != NULL && m3gInRange(index, 0, data->numParams - 1)) {
            const char *src = (const char *)data->params[index];
            M3Gsizei len = data->paramLengths[index];
            if (buffer != NULL) {
                m3gCopy(buffer, src, len);
                return data->paramId[index];
            }
            else
                return len;
        }
        else
            return 0;
    }
}

#undef ANY_NODE_CLASS