/*
 * Copyright © 2003 Anders Carlsson
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of Anders Carlsson not be used in
 * advertising or publicity pertaining to distribution of the software without
 * specific, written prior permission.  Anders Carlsson makes no
 * representations about the suitability of this software for any purpose.  It
 * is provided "as is" without express or implied warranty.
 *
 * ANDERS CARLSSON DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
 * EVENT SHALL ANDERS CARLSSON BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
 * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 */

/** @file
 * This allocator allocates blocks of memory by maintaining a list of areas.
 * When allocating, the contiguous block of areas with the minimum eviction
 * cost is found and evicted in order to make room for the new allocation.
 */

#include "exa_priv.h"

#include <limits.h>
#include <assert.h>
#include <stdlib.h>

#if DEBUG_OFFSCREEN
#define DBG_OFFSCREEN(a) ErrorF a
#else
#define DBG_OFFSCREEN(a)
#endif

#if DEBUG_OFFSCREEN
static void
ExaOffscreenValidate(ScreenPtr pScreen)
{
    ExaScreenPriv(pScreen);
    ExaOffscreenArea *prev = 0, *area;

    assert(pExaScr->info->offScreenAreas->base_offset ==
           pExaScr->info->offScreenBase);
    for (area = pExaScr->info->offScreenAreas; area; area = area->next) {
        assert(area->offset >= area->base_offset &&
               area->offset < (area->base_offset + area->size));
        if (prev)
            assert(prev->base_offset + prev->size == area->base_offset);
        prev = area;
    }
    assert(prev->base_offset + prev->size == pExaScr->info->memorySize);
}
#else
#define ExaOffscreenValidate(s)
#endif

static ExaOffscreenArea *
ExaOffscreenKickOut(ScreenPtr pScreen, ExaOffscreenArea * area)
{
    if (area->save)
        (*area->save) (pScreen, area);
    return exaOffscreenFree(pScreen, area);
}

static void
exaUpdateEvictionCost(ExaOffscreenArea * area, unsigned offScreenCounter)
{
    unsigned age;

    if (area->state == ExaOffscreenAvail)
        return;

    age = offScreenCounter - area->last_use;

    /* This is unlikely to happen, but could result in a division by zero... */
    if (age > (UINT_MAX / 2)) {
        age = UINT_MAX / 2;
        area->last_use = offScreenCounter - age;
    }

    area->eviction_cost = area->size / age;
}

static ExaOffscreenArea *
exaFindAreaToEvict(ExaScreenPrivPtr pExaScr, int size, int align)
{
    ExaOffscreenArea *begin, *end, *best;
    unsigned cost, best_cost;
    int avail, real_size;

    best_cost = UINT_MAX;
    begin = end = pExaScr->info->offScreenAreas;
    avail = 0;
    cost = 0;
    best = 0;

    while (end != NULL) {
 restart:
        while (begin != NULL && begin->state == ExaOffscreenLocked)
            begin = end = begin->next;

        if (begin == NULL)
            break;

        /* adjust size needed to account for alignment loss for this area */
        real_size = size + (begin->base_offset + begin->size - size) % align;

        while (avail < real_size && end != NULL) {
            if (end->state == ExaOffscreenLocked) {
                /* Can't more room here, restart after this locked area */
                avail = 0;
                cost = 0;
                begin = end;
                goto restart;
            }
            avail += end->size;
            exaUpdateEvictionCost(end, pExaScr->offScreenCounter);
            cost += end->eviction_cost;
            end = end->next;
        }

        /* Check the cost, update best */
        if (avail >= real_size && cost < best_cost) {
            best = begin;
            best_cost = cost;
        }

        avail -= begin->size;
        cost -= begin->eviction_cost;
        begin = begin->next;
    }

    return best;
}

/**
 * exaOffscreenAlloc allocates offscreen memory
 *
 * @param pScreen current screen
 * @param size size in bytes of the allocation
 * @param align byte alignment requirement for the offset of the allocated area
 * @param locked whether the allocated area is locked and can't be kicked out
 * @param save callback for when the area is evicted from memory
 * @param privdata private data for the save callback.
 *
 * Allocates offscreen memory from the device associated with pScreen.  size
 * and align deteremine where and how large the allocated area is, and locked
 * will mark whether it should be held in card memory.  privdata may be any
 * pointer for the save callback when the area is removed.
 *
 * Note that locked areas do get evicted on VT switch unless the driver
 * requested version 2.1 or newer behavior.  In that case, the save callback is
 * still called.
 */
ExaOffscreenArea *
exaOffscreenAlloc(ScreenPtr pScreen, int size, int align,
                  Bool locked, ExaOffscreenSaveProc save, void *privData)
{
    ExaOffscreenArea *area;

    ExaScreenPriv(pScreen);
    int real_size = 0, largest_avail = 0;

#if DEBUG_OFFSCREEN
    static int number = 0;

    ErrorF("================= ============ allocating a new pixmap %d\n",
           ++number);
#endif

    ExaOffscreenValidate(pScreen);
    if (!align)
        align = 1;

    if (!size) {
        DBG_OFFSCREEN(("Alloc 0x%x -> EMPTY\n", size));
        return NULL;
    }

    /* throw out requests that cannot fit */
    if (size > (pExaScr->info->memorySize - pExaScr->info->offScreenBase)) {
        DBG_OFFSCREEN(("Alloc 0x%x vs (0x%lx) -> TOBIG\n", size,
                       pExaScr->info->memorySize -
                       pExaScr->info->offScreenBase));
        return NULL;
    }

    /* Try to find a free space that'll fit. */
    for (area = pExaScr->info->offScreenAreas; area; area = area->next) {
        /* skip allocated areas */
        if (area->state != ExaOffscreenAvail)
            continue;

        /* adjust size to match alignment requirement */
        real_size = size + (area->base_offset + area->size - size) % align;

        /* does it fit? */
        if (real_size <= area->size)
            break;

        if (area->size > largest_avail)
            largest_avail = area->size;
    }

    if (!area) {
        area = exaFindAreaToEvict(pExaScr, size, align);

        if (!area) {
            DBG_OFFSCREEN(("Alloc 0x%x -> NOSPACE\n", size));
            /* Could not allocate memory */
            ExaOffscreenValidate(pScreen);
            return NULL;
        }

        /* adjust size needed to account for alignment loss for this area */
        real_size = size + (area->base_offset + area->size - size) % align;

        /*
         * Kick out first area if in use
         */
        if (area->state != ExaOffscreenAvail)
            area = ExaOffscreenKickOut(pScreen, area);
        /*
         * Now get the system to merge the other needed areas together
         */
        while (area->size < real_size) {
            assert(area->next && area->next->state == ExaOffscreenRemovable);
            (void) ExaOffscreenKickOut(pScreen, area->next);
        }
    }

    /* save extra space in new area */
    if (real_size < area->size) {
        ExaOffscreenArea *new_area = malloc(sizeof(ExaOffscreenArea));

        if (!new_area)
            return NULL;
        new_area->base_offset = area->base_offset;

        new_area->offset = new_area->base_offset;
        new_area->align = 0;
        new_area->size = area->size - real_size;
        new_area->state = ExaOffscreenAvail;
        new_area->save = NULL;
        new_area->last_use = 0;
        new_area->eviction_cost = 0;
        new_area->next = area;
        new_area->prev = area->prev;
        if (area->prev->next)
            area->prev->next = new_area;
        else
            pExaScr->info->offScreenAreas = new_area;
        area->prev = new_area;
        area->base_offset = new_area->base_offset + new_area->size;
        area->size = real_size;
    }
    else
        pExaScr->numOffscreenAvailable--;

    /*
     * Mark this area as in use
     */
    if (locked)
        area->state = ExaOffscreenLocked;
    else
        area->state = ExaOffscreenRemovable;
    area->privData = privData;
    area->save = save;
    area->last_use = pExaScr->offScreenCounter++;
    area->offset = (area->base_offset + align - 1);
    area->offset -= area->offset % align;
    area->align = align;

    ExaOffscreenValidate(pScreen);

    DBG_OFFSCREEN(("Alloc 0x%x -> 0x%x (0x%x)\n", size,
                   area->base_offset, area->offset));
    return area;
}

/**
 * Ejects all offscreen areas, and uninitializes the offscreen memory manager.
 */
void
ExaOffscreenSwapOut(ScreenPtr pScreen)
{
    ExaScreenPriv(pScreen);

    ExaOffscreenValidate(pScreen);
    /* loop until a single free area spans the space */
    for (;;) {
        ExaOffscreenArea *area = pExaScr->info->offScreenAreas;

        if (!area)
            break;
        if (area->state == ExaOffscreenAvail) {
            area = area->next;
            if (!area)
                break;
        }
        assert(area->state != ExaOffscreenAvail);
        (void) ExaOffscreenKickOut(pScreen, area);
        ExaOffscreenValidate(pScreen);
    }
    ExaOffscreenValidate(pScreen);
    ExaOffscreenFini(pScreen);
}

/** Ejects all pixmaps managed by EXA. */
static void
ExaOffscreenEjectPixmaps(ScreenPtr pScreen)
{
    ExaScreenPriv(pScreen);

    ExaOffscreenValidate(pScreen);
    /* loop until a single free area spans the space */
    for (;;) {
        ExaOffscreenArea *area;

        for (area = pExaScr->info->offScreenAreas; area != NULL;
             area = area->next) {
            if (area->state == ExaOffscreenRemovable &&
                area->save == exaPixmapSave) {
                (void) ExaOffscreenKickOut(pScreen, area);
                ExaOffscreenValidate(pScreen);
                break;
            }
        }
        if (area == NULL)
            break;
    }
    ExaOffscreenValidate(pScreen);
}

void
ExaOffscreenSwapIn(ScreenPtr pScreen)
{
    exaOffscreenInit(pScreen);
}

/**
 * Prepares EXA for disabling of FB access, or restoring it.
 *
 * In version 2.1, the disabling results in pixmaps being ejected, while other
 * allocations remain.  With this plus the prevention of migration while
 * swappedOut is set, EXA by itself should not cause any access of the
 * framebuffer to occur while swapped out.  Any remaining issues are the
 * responsibility of the driver.
 *
 * Prior to version 2.1, all allocations, including locked ones, are ejected
 * when access is disabled, and the allocator is torn down while swappedOut
 * is set.  This is more drastic, and caused implementation difficulties for
 * many drivers that could otherwise handle the lack of FB access while
 * swapped out.
 */
void
exaEnableDisableFBAccess(ScreenPtr pScreen, Bool enable)
{
    ExaScreenPriv(pScreen);

    if (pExaScr->info->flags & EXA_HANDLES_PIXMAPS)
        return;

    if (!enable && pExaScr->disableFbCount++ == 0) {
        if (pExaScr->info->exa_minor < 1)
            ExaOffscreenSwapOut(pScreen);
        else
            ExaOffscreenEjectPixmaps(pScreen);
        pExaScr->swappedOut = TRUE;
    }

    if (enable && --pExaScr->disableFbCount == 0) {
        if (pExaScr->info->exa_minor < 1)
            ExaOffscreenSwapIn(pScreen);
        pExaScr->swappedOut = FALSE;
    }
}

/* merge the next free area into this one */
static void
ExaOffscreenMerge(ExaScreenPrivPtr pExaScr, ExaOffscreenArea * area)
{
    ExaOffscreenArea *next = area->next;

    /* account for space */
    area->size += next->size;
    /* frob pointer */
    area->next = next->next;
    if (area->next)
        area->next->prev = area;
    else
        pExaScr->info->offScreenAreas->prev = area;
    free(next);

    pExaScr->numOffscreenAvailable--;
}

/**
 * exaOffscreenFree frees an allocation.
 *
 * @param pScreen current screen
 * @param area offscreen area to free
 *
 * exaOffscreenFree frees an allocation created by exaOffscreenAlloc.  Note that
 * the save callback of the area is not called, and it is up to the driver to
 * do any cleanup necessary as a result.
 *
 * @return pointer to the newly freed area. This behavior should not be relied
 * on.
 */
ExaOffscreenArea *
exaOffscreenFree(ScreenPtr pScreen, ExaOffscreenArea * area)
{
    ExaScreenPriv(pScreen);
    ExaOffscreenArea *next = area->next;
    ExaOffscreenArea *prev;

    DBG_OFFSCREEN(("Free 0x%x -> 0x%x (0x%x)\n", area->size,
                   area->base_offset, area->offset));
    ExaOffscreenValidate(pScreen);

    area->state = ExaOffscreenAvail;
    area->save = NULL;
    area->last_use = 0;
    area->eviction_cost = 0;
    /*
     * Find previous area
     */
    if (area == pExaScr->info->offScreenAreas)
        prev = NULL;
    else
        prev = area->prev;

    pExaScr->numOffscreenAvailable++;

    /* link with next area if free */
    if (next && next->state == ExaOffscreenAvail)
        ExaOffscreenMerge(pExaScr, area);

    /* link with prev area if free */
    if (prev && prev->state == ExaOffscreenAvail) {
        area = prev;
        ExaOffscreenMerge(pExaScr, area);
    }

    ExaOffscreenValidate(pScreen);
    DBG_OFFSCREEN(("\tdone freeing\n"));
    return area;
}

void
ExaOffscreenMarkUsed(PixmapPtr pPixmap)
{
    ExaPixmapPriv(pPixmap);
    ExaScreenPriv(pPixmap->drawable.pScreen);

    if (!pExaPixmap || !pExaPixmap->area)
        return;

    pExaPixmap->area->last_use = pExaScr->offScreenCounter++;
}

/**
 * Defragment offscreen memory by compacting allocated areas at the end of it,
 * leaving the total amount of memory available as a single area at the
 * beginning (when there are no pinned allocations).
 */
_X_HIDDEN ExaOffscreenArea *
ExaOffscreenDefragment(ScreenPtr pScreen)
{
    ExaScreenPriv(pScreen);
    ExaOffscreenArea *area, *largest_available = NULL;
    int largest_size = 0;
    PixmapPtr pDstPix;
    ExaPixmapPrivPtr pExaDstPix;

    pDstPix = (*pScreen->CreatePixmap) (pScreen, 0, 0, 0, 0);

    if (!pDstPix)
        return NULL;

    pExaDstPix = ExaGetPixmapPriv(pDstPix);
    pExaDstPix->use_gpu_copy = TRUE;

    for (area = pExaScr->info->offScreenAreas->prev;
         area != pExaScr->info->offScreenAreas;) {
        ExaOffscreenArea *prev = area->prev;
        PixmapPtr pSrcPix;
        ExaPixmapPrivPtr pExaSrcPix;
        Bool save_use_gpu_copy;
        int save_pitch;

        if (area->state != ExaOffscreenAvail ||
            prev->state == ExaOffscreenLocked ||
            (prev->state == ExaOffscreenRemovable &&
             prev->save != exaPixmapSave)) {
            area = prev;
            continue;
        }

        if (prev->state == ExaOffscreenAvail) {
            if (area == largest_available) {
                largest_available = prev;
                largest_size += prev->size;
            }
            area = prev;
            ExaOffscreenMerge(pExaScr, area);
            continue;
        }

        if (area->size > largest_size) {
            largest_available = area;
            largest_size = area->size;
        }

        pSrcPix = prev->privData;
        pExaSrcPix = ExaGetPixmapPriv(pSrcPix);

        pExaDstPix->fb_ptr = pExaScr->info->memoryBase +
            area->base_offset + area->size - prev->size + prev->base_offset -
            prev->offset;
        pExaDstPix->fb_ptr -= (unsigned long) pExaDstPix->fb_ptr % prev->align;

        if (pExaDstPix->fb_ptr <= pExaSrcPix->fb_ptr) {
            area = prev;
            continue;
        }

        if (!(pExaScr->info->flags & EXA_SUPPORTS_OFFSCREEN_OVERLAPS) &&
            (pExaSrcPix->fb_ptr + prev->size) > pExaDstPix->fb_ptr) {
            area = prev;
            continue;
        }

        save_use_gpu_copy = pExaSrcPix->use_gpu_copy;
        save_pitch = pSrcPix->devKind;

        pExaSrcPix->use_gpu_copy = TRUE;
        pSrcPix->devKind = pExaSrcPix->fb_pitch;

        pDstPix->drawable.width = pSrcPix->drawable.width;
        pDstPix->devKind = pSrcPix->devKind;
        pDstPix->drawable.height = pSrcPix->drawable.height;
        pDstPix->drawable.depth = pSrcPix->drawable.depth;
        pDstPix->drawable.bitsPerPixel = pSrcPix->drawable.bitsPerPixel;

        if (!pExaScr->info->PrepareCopy(pSrcPix, pDstPix, -1, -1, GXcopy, ~0)) {
            pExaSrcPix->use_gpu_copy = save_use_gpu_copy;
            pSrcPix->devKind = save_pitch;
            area = prev;
            continue;
        }

        pExaScr->info->Copy(pDstPix, 0, 0, 0, 0, pDstPix->drawable.width,
                            pDstPix->drawable.height);
        pExaScr->info->DoneCopy(pDstPix);
        exaMarkSync(pScreen);

        DBG_OFFSCREEN(("Before swap: prev=0x%08x-0x%08x-0x%08x area=0x%08x-0x%08x-0x%08x\n", prev->base_offset, prev->offset, prev->base_offset + prev->size, area->base_offset, area->offset, area->base_offset + area->size));

        /* Calculate swapped area offsets and sizes */
        area->base_offset = prev->base_offset;
        area->offset = area->base_offset;
        prev->offset += pExaDstPix->fb_ptr - pExaSrcPix->fb_ptr;
        assert(prev->offset >= pExaScr->info->offScreenBase &&
               prev->offset < pExaScr->info->memorySize);
        prev->base_offset = prev->offset;
        if (area->next)
            prev->size = area->next->base_offset - prev->base_offset;
        else
            prev->size = pExaScr->info->memorySize - prev->base_offset;
        area->size = prev->base_offset - area->base_offset;

        DBG_OFFSCREEN(("After swap: area=0x%08x-0x%08x-0x%08x prev=0x%08x-0x%08x-0x%08x\n", area->base_offset, area->offset, area->base_offset + area->size, prev->base_offset, prev->offset, prev->base_offset + prev->size));

        /* Swap areas in list */
        if (area->next)
            area->next->prev = prev;
        else
            pExaScr->info->offScreenAreas->prev = prev;
        if (prev->prev->next)
            prev->prev->next = area;
        else
            pExaScr->info->offScreenAreas = area;
        prev->next = area->next;
        area->next = prev;
        area->prev = prev->prev;
        prev->prev = area;
        if (!area->prev->next)
            pExaScr->info->offScreenAreas = area;

#if DEBUG_OFFSCREEN
        if (prev->prev == prev || prev->next == prev)
            ErrorF("Whoops, prev points to itself!\n");

        if (area->prev == area || area->next == area)
            ErrorF("Whoops, area points to itself!\n");
#endif

        pExaSrcPix->fb_ptr = pExaDstPix->fb_ptr;
        pExaSrcPix->use_gpu_copy = save_use_gpu_copy;
        pSrcPix->devKind = save_pitch;
    }

    pDstPix->drawable.width = 0;
    pDstPix->drawable.height = 0;
    pDstPix->drawable.depth = 0;
    pDstPix->drawable.bitsPerPixel = 0;

    (*pScreen->DestroyPixmap) (pDstPix);

    if (area->state == ExaOffscreenAvail && area->size > largest_size)
        return area;

    return largest_available;
}

/**
 * exaOffscreenInit initializes the offscreen memory manager.
 *
 * @param pScreen current screen
 *
 * exaOffscreenInit is called by exaDriverInit to set up the memory manager for
 * the screen, if any offscreen memory is available.
 */
Bool
exaOffscreenInit(ScreenPtr pScreen)
{
    ExaScreenPriv(pScreen);
    ExaOffscreenArea *area;

    /* Allocate a big free area */
    area = malloc(sizeof(ExaOffscreenArea));

    if (!area)
        return FALSE;

    area->state = ExaOffscreenAvail;
    area->base_offset = pExaScr->info->offScreenBase;
    area->offset = area->base_offset;
    area->align = 0;
    area->size = pExaScr->info->memorySize - area->base_offset;
    area->save = NULL;
    area->next = NULL;
    area->prev = area;
    area->last_use = 0;
    area->eviction_cost = 0;

    /* Add it to the free areas */
    pExaScr->info->offScreenAreas = area;
    pExaScr->offScreenCounter = 1;
    pExaScr->numOffscreenAvailable = 1;

    ExaOffscreenValidate(pScreen);

    return TRUE;
}

void
ExaOffscreenFini(ScreenPtr pScreen)
{
    ExaScreenPriv(pScreen);
    ExaOffscreenArea *area;

    /* just free all of the area records */
    while ((area = pExaScr->info->offScreenAreas)) {
        pExaScr->info->offScreenAreas = area->next;
        free(area);
    }
}
