C

Memory allocation in Qt Quick Ultralite platform abstraction

The Qt Quick Ultralite core library uses the memory allocator API for larger memory allocations. This API provides more control over how and where to allocate memory for different purposes. For example, it might make sense to store image data in VRAM on some platforms. The API also requires the Qt Quick Ultralite core library to explicitly acquire and release memory before and after writing to it, letting the platform do any necessary synchronization.

Example memory allocator

Here's what an example memory allocator might look like:

class ExampleMemoryAllocator : public PlatformInterface::MemoryAllocator
{
public:
    void *allocate(std::size_t size, UsagePattern) QUL_DECL_OVERRIDE
    {
        void *ptr = NULL;
        // ptr = HW_VideoMemAlloc(size);
        return ptr;
    }

    void *reallocate(void *ptr, std::size_t size) QUL_DECL_OVERRIDE
    {
        void *new_ptr = NULL;
        // new_ptr = HW_VideoMemRealloc(ptr, size);
        return new_ptr;
    }

    void free(void *ptr) QUL_DECL_OVERRIDE
    {
        // HW_VideoMemFree(ptr);
    }

    void acquire(void * /* ptr */, std::size_t /* offset */, std::size_t /* size */) QUL_DECL_OVERRIDE
    {
        // WaitForLcdUpdateComplete();
    }
};

In this example, it's assumed that there's a hardware API available to allocate video memory instead of normal memory. Although, a custom platform port might choose to use some other memory regions.

The MemoryAllocator::acquire function override is called before writing to a memory buffer allocated with the PlatformInterface::MemoryAllocator API. Some hardware might have limited memory bandwidth, for example so that SDRAM can't be written to while the framebuffer is being scanned by the LCD display controller. In that case the platform can choose to wait until the LCD update has finished before allowing any writes to SDRAM.

Next, here's the example implementation of the PlatformContext::memoryAllocator function:

PlatformInterface::MemoryAllocator *ExamplePlatform::memoryAllocator(
    PlatformInterface::MemoryAllocator::AllocationType type)
{
    static ExampleMemoryAllocator exampleMemoryAllocator;
    static ExampleReversePreloadAllocator<4> examplePreloadAllocator(preloadSdramEnd, preloadSdramStart);
    static PlatformInterface::MemoryAllocator defaultMemoryAllocator;

    switch (type) {
    case PlatformInterface::MemoryAllocator::Image:
        return &exampleMemoryAllocator;
    case PlatformInterface::MemoryAllocator::DefaultPreload:
        return &examplePreloadAllocator;
    default:
        return &defaultMemoryAllocator;
    }
}

Here we use our example memory allocator from above for image allocations, but fall back to the default implementation for other allocations. The default implementation forwards control to the Platform::qul_malloc, Platform::qul_realloc, and Platform::qul_free functions.

Example reverse preload allocator

Preloadable resources are loaded only once during the platform initialization so we might use a simpler allocator for this purpose. For instance memory reallocation and freeing are not needed so they don't need to be supported. By using reverse allocator the preload memory starts from the end of the section and grows towards the start of the section. This can be useful for example to avoid preload memory to grow in the same direction where the heap memory is located.

Here's what an example reverse preload allocator might look like:

template<std::size_t ALIGNMENT>
class ExampleReversePreloadAllocator : public PlatformInterface::MemoryAllocator
{
public:
    ExampleReversePreloadAllocator(void *endAddress, void *startAddress = NULL)
        : m_currentAddress(endAddress)
        , m_startAddress(startAddress)
    {}

    void *allocate(std::size_t size, UsagePattern) QUL_DECL_OVERRIDE
    {
        m_currentAddress = static_cast<char *>(m_currentAddress) - size;
        m_currentAddress = static_cast<char *>(m_currentAddress)
                           - (reinterpret_cast<uintptr_t>(m_currentAddress) % ALIGNMENT);
        if (m_startAddress && m_currentAddress < m_startAddress)
            return NULL;
        return m_currentAddress;
    }

    // Reallocating is not supported in preloading
    void *reallocate(void *, std::size_t) QUL_DECL_OVERRIDE
    {
        QUL_ASSERT(false, QulError_PreloadAllocator_UnsupportedOperation);
        return NULL;
    };

    // Preallocated assets are not freed
    void free(void *) QUL_DECL_OVERRIDE { QUL_ASSERT(false, QulError_PreloadAllocator_UnsupportedOperation); };
    void acquire(void *, std::size_t, std::size_t) QUL_DECL_OVERRIDE
    {
        QUL_ASSERT(false, QulError_PreloadAllocator_UnsupportedOperation);
    };

    void release(void *, std::size_t, std::size_t) QUL_DECL_OVERRIDE
    {
        QUL_ASSERT(false, QulError_PreloadAllocator_UnsupportedOperation);
    };

private:
    void *m_currentAddress;
    const void *m_startAddress;
};

Fast memory allocations

Use Platform::StackAllocator to allocate short-term memory faster than a regular allocation. An example usage can be found in its class documentation.

Preallocate static memory to prepare the allocator. This is done by reserving a static amount of memory and initializing the l{Qul::Platform::}{StackAllocator}'s static members.

static char qul_scratch_buffer[16 * 1024];

char *StackAllocator::m_buffer = qul_scratch_buffer;
char *StackAllocator::m_top = StackAllocator::m_buffer;
int32_t StackAllocator::m_size = sizeof(qul_scratch_buffer);

The code in the earlier example reserves 16kB of buffer, and sets its pointers and size to the StackAllocator.

Note: The stack allocator must be initialized so that the Qt Quick Ultralite core library can use it. #### FIXME: Why does the core library use it? It seems to be a custom thing.

Available under certain Qt licenses.
Find out more.