/*
 * texture.c    : Device-independent texture caching routines
 *
 * This is an attempt to recode Andy's original texture caching routines
 * into something totally device-independent, capable of handling
 * multiple caching algorithms and generally doing everything much more
 * cleanly than the old system where everyone had to do their own
 * specific hacks into the texture system for their particular
 * 3D card.
 */

#include "texture.h"
#include "drv.h"
/*
 * Texture cache management
 *
 * Thesis/Idiot's Guide/Deranged Ramblings (delete as applicable)
 * by Dave Oldcorn of Volume 11 Software.
 *
 * Managing the texture cache is an important problem (in general on
 * a 2M card in 16-bit mode there will only be about 280k free for
 * texturing.
 *
 * There are two separate cases; the Z-sort and Z-buffered renderers.
 *
 * In the Z-buffered case, cache management is less important because
 * of sort-by-material. Here the cache manager will only help
 * significantly if the working store per frame is approximately the
 * same or somewhat smaller than the texture buffer. If the texture buffer
 * is significantly larger than the working store, the card memory will
 * absorb the lot and no swapping will occur. If the texture memory is much
 * smaller - say less than half the working store - then so much swapping
 * will be going on that the actual algorithm used isn't likely to matter
 * too much.
 *    An efficient caching algorithm will work by trying to maximise
 * the frame-to-frame coherency of the texture buffer. Textures should
 * be freed if they are not going to be used in a frame. In practice,
 * this is unlikely to be achievable, so they should be freed if they
 * haven't been used in the last frame.
 *    It is also an advantage to swap out textures that have already
 * been used for rendering rather than those that have already been used;
 * however, this should be considered carefully as it can ruin the
 * frame-frame coherency described above. I'm not sure if you actually
 * gain or lose anything by considering this point.
 *    A good algorithm for Z-buffered caching would go something like this:
 * Until memory fills, keep filling the card.
 * If there isn't any memory on the card, we need to find space on the card.
 * Look for the least-used texture present on the card and free it.
 *    That algorithm does assume that the textures are all the same size.
 * It would need more consideration for mixed texture sizes.
 *    It works on the principle that if a texture is heavily
 * used in the current frame, it will be heavily used in the next frame,
 * and if it is rarely used in the current frame it will be rarely used
 * in the next frame.
 *    Practical implementations will require a definition for 'how much
 * a texture is used'. The simplest parameter available for tracking is
 * how many polygons it is used on. This is OK, although it will
 * fall down on items such as backgrounds which will tend to be composed
 * of a few large polygons. A pixels-per-frame count would be much better
 * if practically implementable.
 *    If more information was available from the application about the
 * texture type, then a more efficient method could be defined. Possible
 * markings would mark always-required textures, rapidly animated textures
 * such as explosions (which may be large and changing every frame and
 * therefore might be retained in the cache between frames even when not
 * required) - I'm sure I could come up with several more if I thought
 * about it a bit.
 *    All in all, however, the actual algorithm used is a bit less than
 * critical as long as it doesn't leave any textures on the card that
 * won't be required for a while, because every texture that doesn't stay
 * cached has to go into the card at least once per frame - the amount of
 * memory that passes through the card is the same whatever the algorithm
 * is if there aren't any expired textures. The best performance can then
 * be acheived if you aim to swap only the overspill of textures from one
 * frame to the other.
 *    What I have done here is collected 'used' and 'not used' flags
 * and thrown away not used textures in preference. If there aren't any
 * not used textures, then throw away any texture that has already been
 * sent this frame - the exact one isn't really important. If nothing
 * reasonable can be found - only likely for either the first texture per
 * frame or a texture of radically different size to the rest - then
 * it simply discards as many textures as it has to starting at the first.
 *    The least pleasant cases for this algorithm probably involve mixing
 * lots of different texture sizes (unlikely) or using textures that change
 * every frame (likely).
 *
 *
 *
 *    This is not true for the Z-sort renderer. Without the advantages of
 * sort-by-material the textures can arrive in any order, and each texture
 * may be required several times during the course of a frame. This changes
 * the problem significantly. Although it is not quite this bad - because
 * the same materials will tend to be used on the same objects which will
 * have large numbers of polygons at roughly the same Z - there are plenty
 * of problems.
 *    For example, consider the case of a sphere where its halves are mapped
 * with a different texture and the sphere is being viewed so an equal amount
 * of both halves are present. If an algorithm the same as the Zb is used and
 * the buffer is full, the 'left' texture will be used once, then because
 * 1) it has only been used once for a small polygon and 2) it has already
 * been used this frame, it will be swapped out to make way for the 'right'
 * texture, and this process will repeat until the texture buffer melts.
 *    The process is therefore concerned far more with maintaining the
 * inside-one-frame cache coherency than the frame-frame coherency - which
 * although still useful as above is likely to be difficult - again as an
 * example, background textures will be used every frame, but 1) first
 * and 2) only once per frame. So is it a good idea to keep it or not?
 * Your guess is as good as mine.
 *    In general, writing a good Zs texture manager is likely to be tricky
 * and almost anything you can come up with will probably have some appalling
 * pathological cases, particularly if the number of textures cachable is
 * quite low (say < 10 textures of the typical size used by the application).
 *    The total polygon area will probably be the best marker here as well.
 *
 *
 *
 */






/* TEXTURE MEMORY MANAGER */

/* Based on code originally written by whoever wrote malloc
 * Rewritten by Andy to do it in card memory
 * Rewritten by just about everyone else for their own device drivers
 * Rewritten by Dave to genericise it and try to make it coherent again
 */


#if 0
//#include "log.h"
#define memlog0(s)          { syslog(s); }
#define memlog1(s, n)       { syslog(s, n); }
#define memlog2(s, n, n2)   { syslog(s, n, n2); }
#else
#define memlog0(s)
#define memlog1(s, n)
#define memlog2(s, n, n2)
#endif


static br_boolean texture_cache_initialised = BR_FALSE;



static texture_freed_cbfn *texture_callback = NULL;

/* This selects caching mode; this is not currently implemented */
#define CACHE_MODE_ZB   0
#define CACHE_MODE_ZS   1
static br_uint_32 cache_mode = CACHE_MODE_ZB;


/*
 * Block alignment
 */
#define ALIGN	8


/*
 * Pointer to the free and allocated lists of memory blocks
 * Both lists are maintained in card memory order
 *
 * This isn't an ideal solution, because it means that only the adjacent
 * allocated blocks are considered when freeing up multiple textures and
 * not the gaps between them, but it makes the card_mem_free function
 * simpler (i.e. possible to write with some hope that it might work)
 */
static memory_block *free_list = NULL;
static memory_block *alloc_list = NULL;


/*
 * Largest possible space; so for far-too-large textures won't
 * trash the texture cache on every call
 */
static br_uint_32 largest_mem_block = 0;


/*
 * Allocate 'size' bytes of memory from the free list.
 * Memory offsets calculated from texture_base
 *
 * Returns the memory block structure because free requires the pointer
 * to the memory block to organise freeing.
 */
static memory_block * card_mem_alloc(br_uint_32 size)
{
  br_uint_32 total_size;            // Size of block required in card memory
  br_int_32 offset;
  memory_block *fc, *fp, *new;

  /*
   * Find the first free block that's big enough
   */
  total_size = (size+(ALIGN-1))&(~(ALIGN-1));
  fp = NULL;
  fc = free_list;
  if (fc == NULL) {
    memlog0("card_mem_alloc : Memory manager not initialise");
    return(NULL);
  }
   
  memlog1("card_mem_alloc : Block allocation %ld",total_size);
  while(fc != NULL) {
    if (total_size <= fc->size) {
      offset = fc->card_mapping;
      break;
    }
    fp = fc;
    fc = fc->next;
  } 

  /*
   * Return if search unsuccessful
   */
  if (fc == NULL)
    return(NULL);

  memlog2("card_mem_alloc : Block Found, size %ld, offset %ld",fc->size, offset);

  /* fc is our block, offset is its base address in card memory */

  /*
   * Amend linked list of free blocks
   */
  if (total_size == fc->size) {
    /* Size of new and old block are the same (possibly quite likely in the
     * case of textures which are all likely to be the same size) so just
     * unlink the block and return it. */
    if (fp != NULL)
      fp->next = fc->next;
    else
      free_list = fc->next;

    new = fc;

  } else {
    /* Allocate a new memory block and split the old one */
    new = (memory_block *) BrResAllocate(DRIVER_RESOURCE,sizeof(memory_block),BR_MEMORY_SCRATCH);
    if (new == NULL) {
      memlog0("card_mem_alloc : BrResAllocate of new card alloc unit failed!");
      return(NULL);
    }
    fc->size -= total_size;
    fc->card_mapping += total_size;

    memlog1("card_mem_alloc : new block allocated, mapping = %ld", offset);

    new->size = total_size;
    new->card_mapping = offset;
  }

  /* Insert block on allocated list */
  if (!alloc_list) {
    alloc_list = new;
    new->next = NULL;
  } else {
    for(fp=NULL,fc=alloc_list; fc; fp=fc,fc=fc->next)
      if (fc->card_mapping >= new->card_mapping)
        break;
    if (fp == NULL) {
      new->next = alloc_list;
      alloc_list = new;
    } else {
      new->next = fp->next;
      fp->next = new;
    }
  }

  /*
   * Return pointer to memory block describing this memory
   */ 
  return(new);
}



/*
 * Return memory to the free list
 */
static void card_mem_free(memory_block *mem)
{
  memory_block *fc,*fp;

  /* Clear any texture reference in this texture */
  mem->texture = NULL;

  /*
   * Unlink from allocated list
   */
  if (alloc_list == mem)
    alloc_list = mem->next;
  else {
    for(fc=alloc_list; fc; fc=fc->next)
      if (fc->next == mem)
        break;
    if (fc) {   // Shouldn't fail this case but what the hell
      fc->next = mem->next;
    } else {
      memlog0("card_mem_free: Block not on alloc list in free");
    }
  }
  mem->next = NULL;

  /*
   * Catch case of empty free list , used when initialising..
   */
  if (free_list == NULL) {
    memlog0("card_mem_free: empty free list, initialising");
    free_list = mem;
    return;
  }

  /*
   * Find neighbouring free blocks
   */
  fp = NULL;
  fc = free_list;
  while (fc != NULL) {
    if (fc->card_mapping > mem->card_mapping)   // Works because ordered by card mapping
      break;
    fp = fc;
    fc = fc->next;
  }

  /*
   * Merge with following free block if possible
   * 
   * Blocks must be adjacent and within the same 2MB region to merge
   */
  if (fc != NULL) {
    if ( (mem->card_mapping + mem->size) < fc->card_mapping ||
		((mem->card_mapping ^ fc->card_mapping) & -2048 * 1024) != 0) {
      /* Can't merge the blocks - another block has been allocated between
       * this one and the next, so insert it back into the list here. */
      mem->next = fc;
      memlog2("card_mem_free : Can't merge blocks mem = %ld fc = %ld",
                        mem->card_mapping, fc->card_mapping);
    } else {
      /* Next block is in the right place, so merge */
      memlog2("card_mem_free : Merging free blocks size %ld and %ld",mem->size,fc->size);
      memlog2("card_mem_free : At addresses %ld and %ld",mem->card_mapping,fc->card_mapping);
      mem->size += fc->size;
      mem->next = fc->next;
      BrResFree(fc);
    }
  } else
    mem->next = NULL;   // No next block to merge to

  /*
   * Merge with previous free block if possible
   */
  if (fp != NULL) {
    if ( (fp->card_mapping + fp->size) < mem->card_mapping ||
		((mem->card_mapping ^ fp->card_mapping) & -2048 * 1024) != 0) {
      /* Again, can't merge because there are missing blocks beforehand */
      fp->next = mem;
      memlog2("card_mem_free : Can't merge blocks fp = %ld mem = %ld",
                        fp->card_mapping,mem->card_mapping);
    } else {
      /* Previous block ends at this one; match 'em up */
      memlog2("card_mem_free : Merging free blocks size %ld and %ld",fp->size,mem->size);
      memlog2("card_mem_free : At addresses %ld and %ld",fp->card_mapping,mem->card_mapping);
      fp->size += mem->size;
      fp->next = mem->next;
      BrResFree(mem);
    }  
  } else {
    /* Is the first block */
    memlog0("card_mem_free : setting free_list = mem");
    free_list = mem;
  }
}



/*
 * Add an area of memory to the free list
 *
 * This is required to initialise the list. It only Has to be called once,
 * but Can be called as many times as you want.
 *
 * mem is the offset from the start of texture memory
 * size is the block size to allocate
 */
static br_boolean card_mem_add_block(br_uint_32 mem, br_uint_32 size)
{
  memory_block *new;

  memlog1("card_mem_add_block: adding block of %d bytes", size);

  new = (memory_block*) BrResAllocate(DRIVER_RESOURCE,sizeof(memory_block),BR_MEMORY_SCRATCH);

  if (new == NULL)
    return(BR_FALSE);

  if (size > largest_mem_block)
    largest_mem_block = size;

  new->size = size;
  new->card_mapping = mem;
  card_mem_free(new);

  return(BR_TRUE);
}




/*
 * Find out if this new texture is already on the card
 */
static memory_block *card_mem_find(struct render_buffer *texture)
{
  memory_block *mem;

  for(mem=alloc_list; mem; mem=mem->next) {
    if (mem->texture == texture)
      return(mem);
  }

  return(NULL);
}





/*
 * Free a texture - this is the wrapper that additionally calls the
 * texture freeing callback function to inform the higher-level driver
 * what's going on down here.
 */
static br_boolean TextureCacheFree(memory_block *mem)
{
  memlog0("TextureCacheFree:");

  /* Firstly tell the high-level driver */
  if (texture_callback) {
    memlog0("   calling callback");
    if (!(*texture_callback)(mem->texture))
      return(BR_FALSE);
  }

  /* Then free the card memory */
  memlog0("   freeing memory");
  card_mem_free(mem);

  memlog0("   complete");
  return(BR_TRUE);
}



/*
 * Free a contiguous block of memory of size on the card
 */
static br_boolean card_mem_free_space(br_uint_32 size)
{
  memory_block *mem, *ok, *notbad, *next;
  br_uint_32 size_freed;

  memlog0("card_mem_free_space:");
  if (cache_mode == CACHE_MODE_ZB) {
    memlog0("   Zb mode\n   looking for unused in last frame");
    ok = NULL;
    notbad = NULL;
    for(mem=alloc_list; mem; mem=mem->next) {
      /* Best cases are both not used in last frame */
      if (mem->last_frame == 0) {
        /* First check for best case; a texture not used in last frame that
         * is exactly the right size */
        if (mem->size == size)
          break;
        /* OK case is not used in last frame and bigger OR not used in last
         * frame and followed by one also not used in last frame and BOTH
         * bigger - in theory you could keep on checking down the list of
         * next blocks here but that sounds too much like hard work. */
        if (mem->size >= size)
          ok = mem;
        else if (mem->next && (mem->next->last_frame <= 0) &&
                 ((mem->size + mem->next->size) >= size))
          notbad = mem;
      }
    }

    if (!mem && !ok && !notbad) {
      /* OK we didn't find anything by that method; repeat the procedure
       * but this time looking for blocks we've already used this frame.
       * In almost every imaginable case this should find something. */
      memlog0("   looking for already-used this frame");
      for(mem=alloc_list; mem; mem=mem->next) {
        if (mem->last_frame < 0) {
          if (mem->size == size)
            break;
          if (mem->size >= size)
            ok = mem;
          else if (mem->next && (mem->next->last_frame <= 0) &&
                   ((mem->size + mem->next->size) >= size))
            notbad = mem;
        }
      }
      if (!mem && !ok && !notbad) {
        /* STILL not found anything; just free up starting at the first block */
        memlog0("   can't find anything; using first block");
        mem = alloc_list;
      }
    }
    if (!mem) {
      if (!ok) {
        if (notbad)
          memlog0("   third choice found");
        mem = notbad;
      } else {
        memlog0("   second choice found");
        mem = ok;
      }
    } else
      memlog0("   first choice found");

    /* OK here we have the valid pointer - or if not we have to give up */
    if (!mem) {
      memlog0("   nothing found");
      return(BR_FALSE);
    }

    /* Free blocks from here until we have enough or run out of blocks.
     * Even if we run out of blocks, return OK, in the hope that the
     * free blocks in between have supplied the extra memory */
    memlog0("   freeing blocks from choice");
    for(size_freed=0; mem && (size_freed < size); mem=next) {
      next = mem->next;
      size_freed += mem->size;
      TextureCacheFree(mem);
    }

  } else    // Only Zb case implemented; all renderers use it
    return(BR_FALSE);

  memlog0("   complete");
  return(BR_TRUE);
}




/*
 * Set up the texture cache. This should only be called once. To
 * call it more than once, call deinit first.
 *
 * In : base offset in card memory of texture buffer
 *      size of texture memory
 *      Callback function for cache freeing
 *
 * Note that it is not defined how the 'offset' is specified. It is
 * totally transparent inside these routines as to how it is used; it
 * could be the offset from the start of card memory, from the start of
 * the texture buffer in card memory, or anything else. The only
 * limitation is that it must never be negative beween base_mem_offset
 * and size (because TextureCacheRequest returns a negative number on
 * error).
 */
br_boolean TextureCacheInit(texture_freed_cbfn *tcallback)
{
  memlog0("TextureCacheInit:");

  if (texture_cache_initialised)
    return(BR_FALSE);
  texture_cache_initialised = BR_TRUE;

  largest_mem_block = 0;

  texture_callback = tcallback;

  memlog0("   complete");
  return(BR_TRUE);
}


/*
 * Add an extra memory block: format as above.
 *
 * Note if the blocks added separately are contiguous, they WILL be merged
 * when freed unless they are in different 2MB regions.
 *
 * Also note that you shouldn't add blocks that span 2MB regions
 */
br_boolean TextureCacheAddBlock(br_int_32 base_mem_offset, br_uint_32 size)
{
  if (!texture_cache_initialised)
    return(BR_FALSE);
 
  if (!card_mem_add_block(base_mem_offset, size)) {
    memlog0("   failed to add extra memory");
    return(BR_FALSE);
  }

  return(BR_TRUE);
}


/*
 * Free up all the memory and so on
 *
 * If call_callback is set, will call the texture callback for each
 * one it frees, although the result will be ignored
 */
void TextureCacheDeinit(br_boolean call_callback)
{
  memory_block *mem, *next;

  /* Catch failure just in case */
  if (!texture_cache_initialised)
    return;

  memlog0("TextureCacheDeInit:");

  /* If requested, use the callback method when clearing out the cache */
  if (call_callback)
    for(mem=alloc_list; mem; mem=next) {
      next = mem->next;
      TextureCacheFree(mem);
    }

  /* Free the allocated list: if the above function has been called,
   * this simply frees up anything which didn't get freed due to the
   * callback function returning a dodgy value. Bit icky, but since
   * it's about to die anyway, who cares? (FLW) */
  for(mem=alloc_list; mem; mem=next) {
    next = mem->next;
    card_mem_free(mem);
  }

  /* Reset the list manager */
  BrResFree(free_list);
  free_list = NULL;

  memlog0("   complete");
}




/*
 * Called at the end of each frame; goes through the texture buffer
 * resetting all the statistics on the textures
 */
void TextureCacheFrameEnd(void)
{
  memory_block *mem;

  memlog0("TextureCacheFrameEnd:");
  for(mem=free_list; mem; mem=mem->next) {
    mem->last_frame = mem->this_frame;
    mem->this_frame = 0;
  }
  memlog0("   complete");
}




/*
 * Driver requires texture
 *
 * This is the function that must be called every time a particular
 * texture is required. This has a significant overhead because it has
 * to walk the whole free list (in the worst case), but is not
 * a major problem (certainly not for a Z-buffered case anyway).
 *
 * Any failure returned from this routine almost certainly means that
 * the texture will never be cacheable for the current rendering, and
 * further requests to use this texture should be ignored.
 *
 * This also collects the statistics used to manage the texture buffer.
 *
 * In:  BRender's description of the texture, size of texture
 * Out: Offset of texture in card memory, -1 if failed
 */
br_int_32 TextureCacheRequest(struct render_buffer *texture, br_uint_32 size)
{
  memory_block *texmem;

  memlog1("TextureCacheRequest: size %d", size);
  if (largest_mem_block < size) {
    memlog0("   too big for cache so ignoring");
    return(-1);
  }

  memlog0("   doing search");
  texmem = card_mem_find(texture);
  if (!texmem) {
    memlog0("   not found\n   trying to alloc");
    /* Not already on card, so find the memory for it */
    texmem = card_mem_alloc(size);
    if (!texmem) {
      memlog0("   alloc failed, trying to make room");
      /* Hmmm, not enough room, make some */
      if (!card_mem_free_space(size)) {
        memlog0("   failed to make room");
        return(-1);     // Can't
      }
      memlog0("   made room, trying to realloc now");
      texmem = card_mem_alloc(size);
      if (!texmem) {
        memlog0("   failed realloc");
        return(-1);     // Could but didn't?
      }
    }
    memlog0("   card mem alloc success");
    texmem->texture = texture;

    /* Set up default statistics; mark it used this frame, with a special
     * 'just loaded' flag for last frame so it is known that it was loaded
     * this time */
    texmem->this_frame = 1;
    texmem->last_frame = -1;
  } else {
    memlog0("   found");
    /* Increase the statistics */
    texmem->this_frame++;
  }

  memlog0("   complete");
  return(texmem->card_mapping);
}



/*
 * Free directly rather than through callback
 *
 * Provided to allow blocking memory leaks in alloc function
 */
void TextureCacheRequestFree(struct render_buffer *texture)
{
  memory_block *texmem;

  texmem = card_mem_find(texture);
  if (texmem)
    card_mem_free(texmem);
}


/*
 * Remove any occurrences of a particular texture from card memory
 */
void TextureCacheClearEntry(struct render_buffer *texture)
{
  memory_block *mem, *next;

  memlog0("Clearing texture entry");
  for(mem=alloc_list; mem; mem=next) {
    next = mem->next;
    if (mem->texture == texture)
      TextureCacheFree(mem);
  }

  memlog0("   complete");
}



