/***************************************************************************************\

File Name:      mtx_mem.c

Description:    Memory management for MTX devices.

References:     None.

Author:         Karl Lessard    <klessard@matrox.com>

    Copyright (c) 2001, Matrox Graphics Inc.
    All Rights Reserved.

\***************************************************************************************/

#define __NO_VERSION__
#include "mtx_drv.h"

#include <linux/slab.h>

#ifdef KERNEL_2_4
#include <linux/wrapper.h>
#endif

#ifdef AGPBACKEND
/* import agp backend interface */
extern drm_agp_t *agp_backend;
#endif

/* list of agp memory blocks (first item use as sentinel) */
static AGP_Memory agp_blocks = { 0, NULL, NULL, 0, 0, NULL, 0, 0, 0, 0, 0 };

/* this holds real size of last allocation */
size_t last_alloc_size = 0;

#ifdef MEMORY_STATS

typedef struct mtx_mem_info
{
    unsigned long        size;      /* Size of memory chunk */

    const char*          filename;  /* Name of file where allocation was called */
    int                  line;      /* Line number where allocation was called */
    struct mtx_mem_info* prev;      /* Previous memory info record */
    struct mtx_mem_info* next;      /* Next memory info record */
    int64_t              allocation_mark; /* ID gived to allocation */

} mtx_mem_info_t;

static int alloc_count = 0;
static unsigned long alloc_mem = 0;
static int alloc_pci_count = 0;
static unsigned long alloc_pci_mem = 0;
static int alloc_agp_count = 0;
static unsigned long alloc_agp_mem = 0;
static int ioremap_count = 0;

static mtx_mem_info_t mem_info_head = {0, NULL, 0, NULL, NULL, 0};
static int64_t next_allocation_mark = 0;

#endif

/***************************************************************************************
 * Function:       mtx_mem_alloc_pci
 *
 * Description:    Allocate memory in kernel address space and that can be
 *                 accessed directly by a PCI device. It then needs to be
 *                 contiguous in physical memory, and in a memory range
 *                 accessible by DMA.
 *
 * Parameters:     size             Size in bytes of memory space to allocate
 *
 * Return Value:   Ptr to allocated memory, NULL if failed
 *
 * Comments:       We'll use a page oriented scheme. For now, the size must not be
 *                 higher than 2^9 pages. If it do, the allocation failed.
 *
 *                 XXX This method is not safe under interrupt handlers
 */
void* mtx_mem_alloc_pci(size_t size)
{
    unsigned long ptr, kaddr;
    int pg_order, i;

    pg_order = mtx_get_pg_order(size);

    if (pg_order < 0)
        return NULL;

    /* Get the needed number of pages */
    ptr = __get_free_pages(GFP_KERNEL, pg_order);

    if (!ptr) {
        MTX_ERROR("Failed to allocate PCI memory\n");
        return 0;
    }
    
    for (i = 0, kaddr = ptr; i < (1 << pg_order); i++, kaddr += PAGE_SIZE) {

        /* Reserved each pages so they will not be managed by the kernel */
        MemReserve( virt_to_page(kaddr) );
    }

    last_alloc_size = (PAGE_SIZE << pg_order);

    /* Zero the pages */
    memset((void*)ptr, 0, last_alloc_size);

  #if MEMORY_STATS
    ++alloc_pci_count;
    alloc_pci_mem += size;
  #endif
    
    return (void*)ptr;
}

/***************************************************************************************
 * Function:       mtx_mem_free_pci
 *
 * Description:    Free a memory chunk previously allocated by mtx_mem_alloc_pci.
 *
 * Parameters:     ptr      Pointer to memory to free.
 *
 * Return Value:   None.
 *
 * Comments:
 *
 */
void mtx_mem_free_pci(void* ptr, size_t size)
{
    unsigned long kaddr;
    int pg_order, i;

    if (!ptr || !size) {
        MTX_ERROR("Attempt to free NULL pointer\n");
        return;
    }

    if (((unsigned long)ptr & (PAGE_SIZE - 1)) != 0) {
        MTX_ERROR("Attempt to free non paged-aligned PCI pointer\n");
        return;
    }

    pg_order = mtx_get_pg_order(size);
    
    for (i = 0, kaddr = (unsigned long)ptr; i < (1 << pg_order); i++, kaddr += PAGE_SIZE) {

        /* Give back pages to the kernel memory manager */
        MemUnreserve( virt_to_page(kaddr) );
    }

    /* Free pages */
    free_pages((unsigned long)ptr, pg_order);
    
  #if MEMORY_STATS
    --alloc_pci_count;
    alloc_pci_mem -= size;
  #endif
}

/***************************************************************************************
 * Function:       mtx_mem_alloc_agp
 *
 * Description:    Allocate memory in kernel address space and that can be
 *                 accessed directly by a AGP device. It then needs to be
 *                 allocated by the AGP chipset, which take cares of the
 *                 GART table.
 *                 
 * Parameters:     size             Size in bytes of memory space to allocate
 *
 * Return Value:   Base of allocated memory, NULL if failed
 *
 * Comments:
 *
 */
UINT32 mtx_mem_alloc_agp(size_t size)
{
    AGP_Memory *info_block, *ptr_block;
    unsigned long pg_offset = 0, pg_count = 0;
    int ret;
    
    if (!agp_drv || !agp_drv->enabled)
        return 0;
  
#ifdef AGPBACKEND
    if (!agp_backend)
        return 0;
#endif

    pg_count = ((size + (PAGE_SIZE - 1)) & PAGE_MASK) / PAGE_SIZE;
    if (!pg_count)
        return 0;

    /* Alloc AGP memory block */
    info_block = MTX_AGPGART_ALLOCATE_MEMORY(agp_backend, pg_count, AGP_NORMAL_MEMORY);

    if (!info_block) {
        MTX_ERROR("Failed to allocate AGP memory block\n");
        return 0;
    }

    /* Find free space in AGP aperture for our block */
    for (ptr_block = &agp_blocks; ptr_block != NULL; ptr_block = ptr_block->next) {

        if (ptr_block->next == NULL) {

            /* End of list, add block here and bind it after last block in aperture */
            info_block->prev = ptr_block;
            info_block->next = NULL;
            ptr_block->next = info_block;
        
            pg_offset = ptr_block->pg_start + ptr_block->page_count;
                
            break;
                
        } else if (ptr_block->next->pg_start 
                    - (ptr_block->pg_start + ptr_block->page_count) >= pg_count) {

            /* We can fit our block between those two items */
            info_block->prev = ptr_block;
            info_block->next = ptr_block->next;
            ptr_block->next = ptr_block->next->prev = info_block;

            pg_offset = ptr_block->pg_start + ptr_block->page_count;

            break;
        }
    }

    if (!ptr_block) {
        
        MTX_ERROR("Failed to find place for AGP block in aperture\n");
        return 0;
    }

    /* Bind memory block to GATT */
    ret = MTX_AGPGART_BIND_MEMORY(agp_backend, info_block, pg_offset);
    
    if (ret < 0) {
        MTX_ERROR("Failed to bind physical memory to AGP memory block\n");
        return 0;
    }
    
  #if MEMORY_STATS
    ++alloc_agp_count;
    alloc_agp_mem += size;
  #endif

    last_alloc_size = pg_count * PAGE_SIZE;

    return (agp_drv->kern_info.aper_base + (pg_offset << PAGE_SHIFT));
}

/***************************************************************************************
 * Function:       mtx_mem_free_agp
 *
 * Description:    Free a memory chunk previously allocated by mtx_mem_alloc_agp.
 *
 * Parameters:     ptr      Pointer to memory to free.
 *
 * Return Value:   None.
 *
 * Comments:
 *
 */
void mtx_mem_free_agp(unsigned long ptr, size_t size)
{
    AGP_Memory *ptr_block = NULL;
    unsigned int pg_start, pg_count;
    int ret;
    
    if (!agp_drv || !agp_drv->enabled)
        return;

#ifdef AGPBACKEND
    if (!agp_backend)
        return 0;
#endif

    if (((unsigned long)ptr & (PAGE_SIZE - 1)) != 0) {
        MTX_ERROR("Attempt to free non paged-aligned AGP pointer\n");
        return;
    }

    pg_start = ((off_t)ptr - agp_drv->kern_info.aper_base) >> PAGE_SHIFT;
    pg_count = ((size + (PAGE_SIZE - 1)) & PAGE_MASK) / PAGE_SIZE;

    /* Find which block are we trying to release (after sentinel). Attach block to region? */
    for (ptr_block = agp_blocks.next; ptr_block != NULL; ptr_block = ptr_block->next) {

        if (ptr_block->pg_start == pg_start) {

            /* We found it, check if size matches */
            if (ptr_block->page_count != pg_count) {
                MTX_ERROR("Page count doesn't match with AGP block\n");
                return;
            }
            
            /* Unlink block from list */
            if (ptr_block->next)
                ptr_block->next->prev = ptr_block->prev;
                
            if (ptr_block->prev)
                ptr_block->prev->next = ptr_block->next;
            
            break;
        }
    }

    if (!ptr_block) {
        MTX_ERROR("Pointer doesn't match with AGP blocks\n");
        return;
    }

    /* Unbind memory from AGP driver */
    ret = MTX_AGPGART_UNBIND_MEMORY(agp_backend, ptr_block);

    if (ret < 0) {
        MTX_ERROR("Failed to unbind memory block (returned %d)\n", ret);
        return;
    }

    /* Free memory block */
    MTX_AGPGART_FREE_MEMORY(agp_backend, ptr_block);
    
  #if MEMORY_STATS
    --alloc_agp_count;
    alloc_agp_mem -= size;
  #endif
}

/***************************************************************************************
 * Function:       mtx_vmalloc / mtx_free
 *
 * Description:    This free memory allocated for a region. USed by library
 *
 * Parameters:    
 *
 * Return Value:   
 *
 * Comments:
 */
void* mtx_vmalloc(int size)
{
  return vmalloc(size);
}

void mtx_vfree(void* pAddr)
{
  vfree(pAddr);
}

void mtx_enable_dev_space(void *dev_id)
{    
  DWORD dword;
  mtx_device_t *dev = (mtx_device_t*)dev_id;
  pci_read_config_dword(dev->pci_dev, PCI_COMMAND, &dword);
  pci_write_config_dword(dev->pci_dev, PCI_COMMAND, dword | PCI_COMMAND_MEMORY);
}

/***************************************************************************************
 * Function:       mtx_mem_stats_dump
 *
 * Description:    Dump statistics about memory allocation.
 *
 * Parameters:     None.
 *
 * Return Value:   None.
 * 
 * Comments:        
 */
void mtx_mem_stats_dump(void)
{
#if MEMORY_STATS
    int stats_mem = alloc_count * sizeof(mtx_mem_info_t);
    int total_mem = stats_mem + alloc_mem;
    int kb = 0;

    if (total_mem > 1024) {

        /* Print stats in kb */
        stats_mem /= 1024;
        total_mem /= 1024;
        kb = 1;
    }
    
    MTX_PRINT("\n");
    MTX_PRINT("Memory statistics:\n");
    MTX_PRINT(" System alloc count:     %d\n", alloc_count);
    MTX_PRINT(" AGP alloc count:        %d\n", alloc_agp_count);
    MTX_PRINT(" PCI locked alloc count: %d\n", alloc_pci_count);
    MTX_PRINT(" --------------------------\n");
    MTX_PRINT(" Total alloc count       %d\n", alloc_count + alloc_agp_count + alloc_pci_count);
    MTX_PRINT(" I/O map count:          %d\n", ioremap_count);
    MTX_PRINT("\n");
    MTX_PRINT("System memory statistics:\n");
    MTX_PRINT(" Average size (bytes):     %ld\n", alloc_count ? alloc_mem/alloc_count : 0);
    MTX_PRINT(" Stats memory %s     %d, %d%%\n", kb ? "(kb):   " : "(bytes):", 
               stats_mem, total_mem ? (stats_mem * 100) / total_mem : 0);
    MTX_PRINT(" Total memory %s     %d\n", kb ? "(kb):   " : "(bytes):", total_mem);
    MTX_PRINT("\n");
#endif
}

/***************************************************************************************
 * Function:       mtx_mem_stats_dump_list
 *
 * Description:    Dump statistics about memory allocation + the list of allocation
 *                 info record starting at the allocation mark specified.
 *
 * Parameters:     start_mark       Allocation mark where to start list dump
 *
 * Return Value:   None.
 * 
 * Comments:        
 */
void mtx_mem_stats_dump_list(int64_t start_mark)
{
#if MEMORY_STATS
    mtx_mem_info_t *mem_info = mem_info_head.next;
    unsigned long total_dumped = 0, block_count = 0;

    /* Dump memory statistics */
    mtx_mem_stats_dump();
    
    MTX_PRINT(" Dump allocation list from mark %lld\n", start_mark);

    while (mem_info != &mem_info_head) {

        if (mem_info->allocation_mark >= start_mark) {

            MTX_PRINT("  Memory: mark %lld, file \"%s\", line %d, size %ld\n",
                       mem_info->allocation_mark, mem_info->filename, mem_info->line, mem_info->size);

            total_dumped += mem_info->size;
            block_count++;
        }

        mem_info = mem_info->next;
    }

    MTX_PRINT(" %lu block dumped, total memory (kb): %lu\n", block_count, total_dumped);
    MTX_PRINT("\n");
#endif
}
