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

File Name:      mtx_vm.c

Description:    Virtual memory operation on a MTX device file.

References:     None.

Author:         Karl Lessard    <klessard@matrox.com>

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

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

#define __NO_VERSION__
#include "mtx_drv.h"

#if defined (KERNEL_2_4)
#if defined (REDHAT_KERNEL) && \
    ((LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,20)) && \
    (LINUX_VERSION_CODE != KERNEL_VERSION(2,4,22)))
  #define REMAP_PAGE_RANGE remap_page_range
#else
  #define REMAP_PAGE_RANGE(vma, start, base, size, prot) \
    remap_page_range(start, base, size, prot)
#endif
#else
  #if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,10))
     #define REMAP_PAGE_RANGE(vma, uvaddr, paddr, size, prot) \
       remap_pfn_range(vma, uvaddr, paddr >> PAGE_SHIFT, size, prot)
  #else
    #define REMAP_PAGE_RANGE remap_page_range
  #endif
#endif

/* Stub vm operations (noop) */
static struct vm_operations_struct 
mtx_vmops_stub = {
    nopage:     mtx_vm_nopage_stub,
    open:       mtx_vm_open_stub,
    close:      mtx_vm_close_stub,
};

/* System regions vm operations */
static struct vm_operations_struct
mtx_vmops_sys = {
    nopage:     mtx_vm_nopage_sys,
    open:       mtx_vm_open_stub,
    close:      mtx_vm_close_stub,
};

/***************************************************************************************
 * Function:       mtx_vm_nopage_stub
 *
 * Description:    
 * 
 * Parameters:    
 * 
 * Return Value:   NOPAGE_SIGBUS
 * 
 * Comments:       
 */
struct page*
#if defined (KERNEL_2_6)
mtx_vm_nopage_stub(struct vm_area_struct *vma, unsigned long address, int *type)
#else 
mtx_vm_nopage_stub(struct vm_area_struct *vma, unsigned long address, int write_access)
#endif
{
#if defined (KERNEL_2_6)
    if (type != NULL)
        *type = VM_FAULT_MINOR;
#endif

    return NOPAGE_SIGBUS;   /* mremap is not allowed */
}

/***************************************************************************************
 * Function:       mtx_vm_open_stub
 *
 * Description:    Called everytime a reference to this VMA is opened by a process 
 *                 (first process to request the VMA, or its child processes by forking).  
 *
 * Parameters:     vma              pointer to the vma structure
 *
 * Return Value:   None.
 *
 * Comments:       
 */
void
mtx_vm_open_stub(struct vm_area_struct *vma)
{
    /* Nothing needs to be done */
}

/***************************************************************************************
 * Function:       mtx_vm_close_stub
 *
 * Description:    Called everytime a process close a reference to the VMA.
 *
 * Parameters:     vma              pointer to the vma structure
 *
 * Return Value:   None.
 *
 * Comments:       
 */
void
mtx_vm_close_stub(struct vm_area_struct *vma)
{
    /* Nothing needs to be done */
}

/***************************************************************************************
 * Function:       mtx_vm_nopage_sys
 *
 * Description:    Return the missing page for system memory mappings.
 *
 * Parameters:     vma              pointer to the vma structure
 *                 address          first address of faulting page
 *                 write_access     permissions
 *
 * Return Value:   Page structure for the faulting address, or NOPAGE
 *
 * Comments:       
 */
struct page*
#if defined (KERNEL_2_6)
mtx_vm_nopage_sys(struct vm_area_struct *vma, unsigned long address, int *type)
#else
mtx_vm_nopage_sys(struct vm_area_struct *vma, unsigned long address, int write_access)
#endif
{
    struct page *page;
    mtx_region_t *region;
    unsigned long offset;
    unsigned long kaddr;
    pgd_t *pgd; 
    pmd_t *pmd; 
    pte_t *pte;

#if defined (KERNEL_2_6)
    if (type != NULL)
        *type = VM_FAULT_MINOR;
#endif

    /* retrieve the region for this vma */
    region = (mtx_region_t*)vma->vm_private_data;
    if (!region) return NOPAGE_SIGBUS;
        
    /* retrieve offset in this region we want to map */
    offset = address - vma->vm_start;
        
    /* retrieve kernel virtual address at this offset */
    kaddr = (unsigned long) ((u8*)region->kaddr + offset);
    if (!kaddr)
        return NOPAGE_SIGBUS;

    /* Get page by going through the page table */
    spin_lock(&init_mm.page_table_lock);

    pgd = pgd_offset_k(kaddr); if (pgd_none(*pgd)) return NULL;
    pmd = pmd_offset(pgd, kaddr); if (pmd_none(*pmd)) return NULL;
  #ifdef pte_offset_map
    pte = pte_offset_map(pmd, kaddr); if (!pte_present(*pte)) return NULL;
    page = pte_page(*pte); if (!page) return NOPAGE_SIGBUS;
    pte_unmap(pte);
  #else
    pte = pte_offset(pmd, kaddr); if (!pte_present(*pte)) return NULL;
    page = pte_page(*pte); if (!page) return NOPAGE_SIGBUS;
  #endif

    spin_unlock(&init_mm.page_table_lock);

    if ( ! ValidPage(page) )
    {
        MTX_ERROR("mtx_vm_nopage_sys: Virtual address 0x%08lx is beyond paged memory\n", kaddr);
        return NOPAGE_OOM;
    }
    
    /* lock page and return it */
    get_page(page);
    return page;
}


/***************************************************************************************
 * Function:       mtx_vm_map_io
 *
 * Description:    Mapping I/O memory to user virtual space, from a vm_area structure
 *                 filled by the kernel. The only thing we need to do here is to 
 *                 create a table of pages that points to physical memory for every 
 *                 virtual addresses in the vm_area_struct.
 *
 * Parameters:     dev              MTX device
 *                 region           pointer to our memory region
 *                 vma              pointer to the vma structure
 *
 * Return Value:   0 if succeed, errno otherwise.
 *
 * Comments:       
 */
int
mtx_vm_map_io(mtx_device_t *dev, mtx_region_t* region, struct vm_area_struct *vma)
{
    size_t size = vma->vm_end - vma->vm_start;
    
    /* address mapped must be in I/O memory */    
    if (region->base < __pa(high_memory))
        return -EINVAL;
            
    /* Size of region contain whole mapping */
    if (region->real_size < size)
        return -EINVAL;
    
    /* this vma points to I/O memory and should never be swapped out */
    vma->vm_flags |= VM_IO | VM_RESERVED;
  
    if (REMAP_PAGE_RANGE(vma, vma->vm_start, region->base, size, vma->vm_page_prot))
        return -EAGAIN;
    
    /* Use stub vm operations */
    vma->vm_ops = &mtx_vmops_stub;
    
    return 0;
}

/***************************************************************************************
 * Function:       mtx_vm_map_agp
 *
 * Description:    Mapping AGP memory to user virtual space, from a vm_area structure
 *                 filled by the kernel. The only thing we need to do here is to 
 *                 create a table of pages that points to physical memory for every 
 *                 virtual addresses in the vm_area_struct.
 *
 * Parameters:     dev              MTX device
 *                 region           pointer to our memory region
 *                 vma              pointer to the vma structure
 *
 * Return Value:   0 if succeed, errno otherwise.
 *
 * Comments:       
 */
int
mtx_vm_map_agp(mtx_device_t *dev, mtx_region_t *region, struct vm_area_struct *vma)
{
    size_t size = vma->vm_end - vma->vm_start;
        
    /* address mapped must be in I/O memory */    
    if (region->base < __pa(high_memory))
        return -EINVAL;
    
    /* Size of region contain whole mapping */
    if (region->real_size < size)
        return -EINVAL;
    
    /* should never be swapped out */
    vma->vm_flags |= VM_IO | VM_RESERVED;
    
    if (REMAP_PAGE_RANGE(vma, vma->vm_start, region->base, size, vma->vm_page_prot))
        return -EAGAIN;
    
    /* Use stub vm operations */
    vma->vm_ops = &mtx_vmops_stub;

    return 0;
}

/***************************************************************************************
 * Function:       mtx_vm_map_pci
 *
 * Description:    Mapping PCI locked memory to user virtual space, from a vm_area 
 *                 structure filled by the kernel. In this case, we can be sure that
 *                 the memory space is page-aligned, contiguous, and cannot be swapped
 *                 out by the kernel.
 *                 
 * Parameters:     dev              MTX device
 *                 region           pointer to our memory region
 *                 vma              pointer to the vma structure
 *
 * Return Value:   0 if succeed, errno otherwise.
 *
 * Comments:       
 */
int
mtx_vm_map_pci(mtx_device_t *dev, mtx_region_t *region, struct vm_area_struct *vma)
{
    size_t size = vma->vm_end - vma->vm_start;

    /* Size of region contain whole mapping */
    if (region->real_size < size)
        return -EINVAL;

    vma->vm_flags |= VM_RESERVED;
    
    if (REMAP_PAGE_RANGE(vma, vma->vm_start, region->base, size, vma->vm_page_prot))
        return -EAGAIN;

    /* Use stub vm operations */
    vma->vm_ops = &mtx_vmops_stub;

    return 0;
}

/***************************************************************************************
 * Function:       mtx_vm_map_sys
 *
 * Description:    Map an area allocated in memory previously allocated by the kernel
 *                 with the standard kmalloc (or vmalloc) allocation scheme.
 *                 
 * Parameters:     dev              MTX device
 *                 region           pointer to our memory region
 *                 vma              pointer to the vma structure
 *
 * Return Value:   0 if succeed, errno otherwise.
 *
 * Comments:       
 */
int
mtx_vm_map_sys(mtx_device_t *dev, mtx_region_t *region, struct vm_area_struct *vma)
{
    size_t size = vma->vm_end - vma->vm_start;

    /* Size of region contain whole mapping */
    if (region->real_size < size)
        return -EINVAL;
    
    vma->vm_flags |= VM_RESERVED;

    if (region->size <= KMALLOC_MAXSIZE) {
    
        /* Region is contiguous, remap page range now */
        if (REMAP_PAGE_RANGE(vma, vma->vm_start, region->base, size, vma->vm_page_prot))
        {
            MTX_ERROR("Failed to map region at 0x%08lx, size: %lu\n", region->base, region->size);
            return -EAGAIN;
        }

        /* Use stub vm operations */
        vma->vm_ops = &mtx_vmops_stub;
        
    } else {

        /* attach our region to the vma private field */
        vma->vm_private_data = (void*)region;
        
        /* Use system vm operations */
        vma->vm_ops = &mtx_vmops_sys;
    }
    
    return 0;
}
