/* Copyright (c) 1998-1999 Microsoft Corporation */
/* @doc DMusic16
 *
 * @module Alloc.c - Memory allocation routines |
 *
 * This module provides memory allocation routines for DMusic16.DLL. It allows the MIDI input and
 * output modules to allocated and free <c EVENT> structures.
 *
 * The allocated recognizes two types of events by size. If an event is create with 4 or less bytes
 * of data, then it is allocated as a channel message. Channel message events are allocated one
 * page at a time and kept in a free list.
 *
 * If the event size is greater than 4 bytes, then the event is a system exclusive message (or long
 * data in the legacy API nomenclature). These events are allocated individually, one per page.
 *
 * All allocated memory is preceded with a <c SEGHDR>, which is used to identify the size and type
 * of the segment and to keep it in a list. Since all events will be accessed at event time (in
 * either a MIDI input callback or a timeSetEvent callback), all memory is automatically page
 * locked.
 *
 * @globalv WORD | gsSegList |Selector of first segment in allocated list
 * @globalv LPEVENT | glpFreeEventList | List of free 4-byte events 
 * @globalv LPEVENT | glpFreeBigEventList | List of free 4-byte events 
 */
#include <windows.h>
#include <mmsystem.h>
#include <memory.h>

#include "dmusic16.h"
#include "debug.h"

STATIC WORD gsSegList;
STATIC LPEVENT glpFreeEventList;        
STATIC LPEVENT glpFreeBigEventList;     

/* Given a far pointer, get its selector.
 */
#define SEL_OF(lp) (WORD)((((DWORD)lp) >> 16) & 0xffff)

/* Given a far event pointer, get the far pointer to its segment headear.
 */
#define SEGHDR_OF(lp)   ((LPSEGHDR)(((DWORD)lp) & 0xffff0000l))

STATIC BOOL RefillEventList(VOID);
STATIC LPSEGHDR AllocSeg(WORD cbSeg);
STATIC VOID FreeBigEvents(VOID);
STATIC VOID FreeSeg(LPSEGHDR lpSeg);

/* @func Called at DLL LibInit
 *
 * @comm
 * Initializes all free lists to empty.
 *
 */
VOID PASCAL
AllocOnLoad(VOID)
{
    gsSegList = 0;
    glpFreeEventList = NULL;
    glpFreeBigEventList = NULL;
}

/* @func Called at DLL LibExit
 *
 * @comm
 * Unlock and free all of the memory allocated.
 *
 * AllocOnUnload jettisons all memory the allocator has ever allocated. 
 * It assumes that all pointers to events will no longer ever be touched (i.e. all callbacks must
 * have already been disabled by this point).
 */
VOID PASCAL
AllocOnExit(VOID)
{
    WORD sSel;
    WORD sSelNext;
    LPSEGHDR lpSeg;

    sSel = gsSegList;

    while (sSel)
    {
        lpSeg = (LPSEGHDR)(((DWORD)sSel) << 16);
        sSelNext = lpSeg->selNext;
        
        FreeSeg(lpSeg);

        sSel = sSelNext;
    }
    
    /* This just invalidated both free lists as well as the segment list
     */
    gsSegList = 0;
    glpFreeEventList = NULL;
    glpFreeBigEventList = NULL;
}

/* @func Allocate an event of a given size
 *
 * @rdesc Returns a far pointer to the event or NULL if memory could not be allocated.
 *
 * @comm
 *
 * This function is not callable at interrupt time.
 *
 * This function is called to allocate a single event. The event will be allocated from
 * page-locked memory and filled with the given event data.
 *
 * Events are classified as normal events, which contain channel messages, and big events,
 * which contain SysEx data. The two are distinguished by their size: any event containing
 * a DWORD of data or less is a normal event.
 *
 * Since channel messages comprise most of the MIDI stream, allocation of these events is optimized.
 * A segment is allocated containing approximately one page worth (4k) of 4-byte events. These
 * events are doled out of a free pool, which only occasionally needs to be refilled from system
 * memory.
 *
 * Big events are allocated on an as-needed basis. When they have been free'd by a call to FreeEvent,
 * they are placed on a special free list. This list is used to find memory for future big events,
 * and is occasionally free'd back to Windows on a call to AllocEvent in order to minimize the
 * amount of page-locked memory in use.
 */
LPEVENT PASCAL
AllocEvent(
    DWORD msTime,           /* @parm The absolute time based on timeGetTime() of the event */
    QUADWORD rtTime,        /* @parm The absolute time based on the IRferenceClock in 100ns units */
    WORD cbEvent)           /* @parm The number of bytes of event data in pbData */
{
    LPEVENT lpEvent;
    LPEVENT lpEventPrev;
    LPEVENT lpEventCurr;
    LPSEGHDR lpSeg;
    
    /* Check for big event first (Sysex)
     */
    if (cbEvent > sizeof(DWORD))
    {
        /* First see if we have an event that will work already
         */
        lpEventPrev = NULL;
        lpEventCurr = glpFreeBigEventList;
        
        while (lpEventCurr)
        {
            if (SEGHDR_OF(lpEventCurr)->cbSeg >= sizeof(EVENT) + cbEvent)
            {
                break;
            }
            lpEventPrev = lpEventCurr;
            lpEventCurr = lpEventCurr->lpNext;
        }

        if (lpEventCurr)
        {
            /* Remove this event from the list and use it
             */
            if (lpEventPrev)
            {
                lpEventPrev->lpNext = lpEventCurr->lpNext;
            }
            else
            {
                glpFreeBigEventList = lpEventCurr->lpNext;
            }

            lpEventCurr->lpNext = NULL;
        }
        else
        {
            /* Nope, need to allocate one
             */
            lpSeg = AllocSeg(sizeof(EVENT) + cbEvent);
            if (NULL == lpSeg)
            {
                return NULL;
            }

            lpEventCurr = (LPEVENT)(lpSeg + 1);
        }

        lpEventCurr->msTime = msTime;
        lpEventCurr->rtTime = rtTime;
        lpEventCurr->wFlags = 0;
        lpEventCurr->cbEvent = cbEvent;

        return lpEventCurr;
    }

    /* BUGBUG How often???
     */
    FreeBigEvents();

    /* Normal event. Pull it off the free list (refill if needed) and fill it in.
     */
    if (NULL == glpFreeEventList)
    {
        if (!RefillEventList())
        {
            return NULL;
        }
    }

    lpEvent = glpFreeEventList;
    glpFreeEventList = lpEvent->lpNext;

    lpEvent->msTime = msTime;
    lpEvent->rtTime = rtTime;
    lpEvent->wFlags = 0;
    lpEvent->cbEvent = cbEvent;

    return lpEvent;
}

/* @func Free an event back to its appropriate free list
 *
 * @comm
 *
 * FreeEvent makes no system calls; it simply places the given event back on the correct
 * free list. If the event needs to be actually free'd, that will be done at a later time
 * in user mode.
 */
VOID PASCAL
FreeEvent(
    LPEVENT lpEvent)            /* @parm The event to free */
{
    LPSEGHDR lpSeg;

    lpSeg = SEGHDR_OF(lpEvent);
    if (lpSeg->wFlags & SEG_F_4BYTE_EVENTS)
    {
        lpEvent->lpNext = glpFreeEventList;
        glpFreeEventList = lpEvent;
    }
    else
    {
        lpEvent->lpNext = glpFreeBigEventList;
        glpFreeBigEventList = lpEvent;
    }
}

/* @func Refill the free list of normal events
 *
 * @rdesc Returns TRUE if the list was refilled or FALSE if there was no memory.
 *
 * @comm
 *
 * This routine is not callable from interrupt time.
 *
 * Allocate one page-sized segment of normal events and add them to the free list.
 *
 */
STATIC BOOL
RefillEventList(VOID)
{
    LPSEGHDR lpSeg;
    LPEVENT lpEvent;
    UINT cbEvent;
    UINT idx;

    cbEvent = sizeof(EVENT) + sizeof(DWORD);
    lpSeg = AllocSeg(C_PER_SEG * cbEvent);
    if (NULL == lpSeg)
    {
        return FALSE;
    }

    lpSeg->wFlags = SEG_F_4BYTE_EVENTS;

    /* Put the events into the free pool
     */
    lpEvent = (LPEVENT)(lpSeg + 1);

    for (idx = C_PER_SEG - 1; idx; --idx)
    {
        lpEvent->lpNext = (LPEVENT)(((LPBYTE)lpEvent) + cbEvent);
        lpEvent = lpEvent->lpNext;
    }

    lpEvent->lpNext = glpFreeEventList;
    glpFreeEventList = (LPEVENT)(lpSeg + 1);
                                 
    return TRUE;
}

/* @func Free all big events
 *
 * @comm
 *
 * This function is not callable at interrupt time.
 *
 * This function frees all big events on the free big event list. Free big events are those
 * with event data sizes of more than one DWORD; they are allocated one event per segment
 * as needed rather than being pooled like channel messages.
 *
 * This function is called every now and then as a side effect of AllocEvent in order to
 * free up the page-locked memory associated with completed big events.
 *
 */ 
STATIC VOID
FreeBigEvents(VOID)
{
    LPEVENT lpEvent;
    LPEVENT lpEventNext;
    LPSEGHDR lpSeg;

    lpEvent = glpFreeBigEventList;
    while (lpEvent)
    {
        lpEventNext = lpEvent->lpNext;

        lpSeg = SEGHDR_OF(lpEvent);
        FreeSeg(lpSeg);

        lpEvent = lpEventNext;
    }

    glpFreeBigEventList = NULL;
}

/* @func Allocate a segment and put it into the list of allocated segments.
 *
 * @rdesc A far pointer to the segment header or NULL if the memory could not be allocated.
 *
 * @comm
 *
 * This function is not callable at interrupt time.
 *
 * This is the lowest-level allocation routine which actually calls Windows to allocate the memory.
 * The caller is responsible for carving the memory into one or more events.
 *
 * The data area of the segment will be filled with zeroes.
 *
 * Since events are accessed at interrupt time (timeSetEvent callback), the memory is allocated and
 * page locked.
 *
 * This routine also inserts the segment into the global list of allocated segments for cleanup.
 */
STATIC LPSEGHDR
AllocSeg(
    WORD cbSeg)                 /* @parm The size of data needed in the segment, excluding the segment header */
{
    HANDLE hSeg;
    WORD sSegHdr;
    LPSEGHDR lpSeg;

    /* Allocate and page-lock a segment
     * NOTE: GPTR contains zero-init
     */
    cbSeg += sizeof(SEGHDR);
    hSeg = GlobalAlloc(GPTR | GMEM_SHARE, cbSeg);
    if (0 == hSeg)
    {
        return NULL;
    }

    lpSeg = (LPSEGHDR)GlobalLock(hSeg);
    if (NULL == lpSeg)
    {
        GlobalFree(sSegHdr);
        return NULL;
    }

    sSegHdr = SEL_OF(lpSeg);
    if (!GlobalSmartPageLock(sSegHdr))
    {
        GlobalUnlock(sSegHdr);
        GlobalFree(sSegHdr);
        return NULL;
    }

    lpSeg->hSeg = hSeg;
    lpSeg->cbSeg = cbSeg;

    lpSeg->selNext  = gsSegList;
    gsSegList = sSegHdr;
    
    return lpSeg;
}

/* @func Free a segment back to Windows
 *
 * @comm
 *
 * This function is not callable at interrupt time.
 *
 * Just unlock the segment and free it. The calling cleanup code is assumed to have removed
 * the segment from the global list of allocated segments.
 *
 */
STATIC VOID FreeSeg(
    LPSEGHDR lpSeg)         /* @parm The segment to free */
{
    WORD sSel = SEL_OF(lpSeg);
    HANDLE hSeg;
    
    hSeg = lpSeg->hSeg;
    
    GlobalSmartPageUnlock(sSel);
    GlobalUnlock(hSeg);
    GlobalFree(hSeg);
}