/*
 **************************************************************************
 *
 * Network driver interface for netboot bootrom
 *
 * Module:  memory.S86
 * Purpose: Memory management functions for DOS simulator
 * Entries: dos48, dos49, dos4A, freeall, checkmcb
 *
 **************************************************************************
 *
 * Copyright (C) 1995-2007 Gero Kuhlmann <gero@gkminix.han.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: memory.S86,v 1.7 2007/01/06 18:30:48 gkminix Exp $
 *
 */


/*
 **************************************************************************
 *
 * Include assembler macros:
 *
 */
#include <common.i86>
#include <romconfig.i86>
#include <dos/errors.i86>
#include <dos/memory.i86>
#include <system/bootrom.i86>
#include "dossim.i86"

.file __FILE__
.line __LINE__



/*
 **************************************************************************
 *
 * BSS segment
 *
 */
	.bss

	.extern	curr_psp		# current PSP segment

	.global	first_mcb
	.global	memory_end


	.lcomm	first_mcb,2		# pointer to first MCB
	.lcomm	memory_end,2		# first segment past end of memory




/*
 **************************************************************************
 *
 * Start code segment.
 *
 */
	.text

	.global	dos48			# define entry points
	.global	dos49
	.global	dos4A
	.global	dos58
	.global	freeall
	.global	checkmcb



/*
 **************************************************************************
 *
 * Allocate memory block. This routine is very easy in that it allocates
 * the first possible block it can find. This is not very economical, but
 * sufficient for the DOS simulator.
 * Note that this routine might also get called from outside a DOS interrupt,
 * so it should not use any reference to the DOS stack structure!
 * Input:  BX  -  number of paragraphs
 * Output: AX  -  segment address or error code, if carry flag set
 *         BX  -  maximum available number of paragraphs
 *         Carry flag  -  set if error
 * Registers changed: AX, BX
 *
 */
dos48:

	push	fs
	push	dx
	xor	dx,dx			# size of largest available block
	mov	fs,[first_mcb]		# start looking at first MCB
	call	checkmcb		# check the first MCB
	jc	9f

1:	mov	ax,fs:[mcb_owner]
	or	ax,ax			# is block available?
	jz	3f
4:	call	nextmcb			# no, jump to next MCB
	jnc	1b
	or	ax,ax
	jnz	2f			# serious error
	mov	bx,dx			# end of list reached, return largest
	mov	ax,DOS_ERR_NOMEM	# available memory block size
2:	stc				# return with error
	jmp	9f

3:	mov	ax,fs:[mcb_size]
	cmp	bx,0xFFFF		# FFFFh size means: determine amount of
	je	7f			# free memory. never get that much.
	cmp	ax,bx			# check size of free memory block
	jae	5f
7:	cmp	ax,dx			# not sufficient, save in DX if greater
	jbe	4b			# than any preceding block
	mov	dx,ax
	jmp	4b			# continue with next memory block

5:	mov	dx,ax			# block is large enough, save its old
	mov	fs:[mcb_size],bx	# size and then set the new one
	mov	ax,[curr_psp]
	or	ax,ax			# is a process running already
	jnz	6f
	mov	ax,0xFFFF		# dummy PSP address
6:	mov	fs:[mcb_owner],ax	# save owner of memory block
	sub	dx,bx			# compute size of remaining free block
	jbe	8f			# nothing left

	mov	ax,fs
	add	ax,bx			# compute address of new MCB
	inc	ax
	dec	dx			# adjust size of remaining memory block
	mov	bl,MCB_MEM_ID
	xchg	bl,fs:[mcb_id]		# remember old ID and set new ID
	push	ds
	mov	ds,ax			# setup new free memory block
	mov	byte ptr ds:[mcb_id],bl
	mov	word ptr ds:[mcb_size],dx
	mov	word ptr ds:[mcb_owner],0
	pop	ds
	mov	bx,fs:[mcb_size]	# restore BX register
	call	cleanup			# cleanup free memory blocks

8:	mov	ax,fs			# return allocate memory block segment
	inc	ax			# to caller
	clc
9:	pop	dx
	pop	fs
	ret



/*
 **************************************************************************
 *
 * Free a memory block.
 * Note that this routine might also get called from outside a DOS interrupt,
 * so it should not use any reference to the DOS stack structure!
 * Input:  ES  -  segment of memory block to free
 * Output: AX  -  error code if carry flag set
 *         Carry flag  -  set if error
 * Registers changed: AX
 *
 */
dos49:

	push	fs
	push	dx
	push	ax
	mov	dx,es
	dec	dx			# convert memory block address into
	call	findmcb			# MCB address and find MCB
	jc	7f
	mov	word ptr fs:[mcb_owner],0
	call	cleanup			# integrate the new free memory block
7:	pop	dx
	jc	9f			# on error dont restore AX register
	mov	ax,dx
9:	pop	dx
	pop	fs
	ret



/*
 **************************************************************************
 *
 * Resize memory block.
 * Note that this routine might also get called from outside a DOS interrupt,
 * so it should not use any reference to the DOS stack structure!
 * Input:  BX  -  new size of memory block in paragraphs
 *         ES  -  segment of memory block
 * Output: BX  -  maximum available memory space
 *         AX  -  error code if carry flag set
 *         Carry flag  -  set if error
 * Registers changed: AX, BX
 *
 */
dos4A:

	push	fs
	push	dx
	push	ax
	mov	dx,es
	dec	dx			# convert memory block address into
	call	findmcb			# MCB address and find MCB
	jc	7f

	mov	dx,fs:[mcb_size]	# used at label 4
	cmp	bx,dx
	je	8f			# nothing changes
	jb	4f			# make it smaller

/*
 * Make the memory block larger. To do this, first check if the next
 * block has enough memory space available. Then include it into the
 * current block.
 */

	mov	dx,fs
	call	nextmcb			# check next MCB
	jnc	2f
	or	ax,ax
	jnz	7f
1:	mov	ax,DOS_ERR_NOMEM	# when at the end of the memory,
	jmp	7f			# there is no more available

2:	cmp	word ptr fs:[mcb_owner],0
	jne	1b			# is the next memory block free?
	push	dx
	mov	ax,fs:[mcb_size]	# load size of free memory block
	mov	dl,fs:[mcb_id]		# load ID of free memory block
	pop	fs
	add	ax,fs:[mcb_size]	# compute size of both memory blocks
	inc	ax			# combined
	cmp	ax,bx			# is it enough
	jae	3f
	mov	bx,ax			# return maximum size to caller
	jmp	1b

3:	mov	fs:[mcb_size],ax	# set size of new combined segment
	mov	fs:[mcb_id],dl		# set ID of new segment
	cmp	ax,bx			# if the size is exactly what has
	je	8f			# been requested, we dont need to split
	mov	dx,ax

/*
 * Make the memory block smaller.
 */

4:	sub	dx,bx			# compute size of remaining memory block
	dec	dx
	jz	8f			# new memory block is zero size
	mov	fs:[mcb_size],bx	# set new size of memory block
	mov	ax,fs
	add	ax,bx			# compute address of new MCB
	inc	ax
	mov	bl,MCB_MEM_ID
	xchg	bl,fs:[mcb_id]		# remember old ID and set new ID
	push	ds
	mov	ds,ax			# setup new free memory block
	mov	byte ptr ds:[mcb_id],bl
	mov	word ptr ds:[mcb_size],dx
	mov	word ptr ds:[mcb_owner],0
	pop	ds
	mov	bx,fs:[mcb_size]	# restore BX register
	call	cleanup			# cleanup free memory blocks
	jnc	8f

/*
 * Termination with or without error.
 */

7:	stc				# return with error
	pop	dx			# dont restore AX
	jmp	9f

8:	clc				# no error
	pop	ax
9:	pop	dx
	pop	fs
	ret



/*
 **************************************************************************
 *
 * Set or get the DOS memory allocation strategy.
 * Since we always use the same allocation strategy, this function always
 * returns the same value. In addition to the original DOS we introduce
 * another function to return the segment of the first MCB.
 * Input:  AL  -  Function code
 *         BL  -  New memory allocation strategy
 * Output: AX  -  Old memory allocation strategy
 *         Carry flag  -  set if error
 * Registers changed: AX
 *
 */
dos58:

	xor	ah,ah
	or	al,al			# return current allocation strategy, which
	jz	9f			# is always 0 (e.g. low memory first fit)
	cmp	al,0x63			# our own function to return first MCB seg
	jne	8f
	mov	ax,[first_mcb]		# return first MCB segment
	jmp	9f

8:	stc
	mov	al,1			# function not supported
9:	ret



/*
 **************************************************************************
 *
 * Free all memory blocks of an owner. This routine is called internally
 * by the process management code.
 * Input:  DX  -  PSP segment of owner
 * Output: AX  -  error code if carry flag set
 *         CX  -  number of MCBs freed
 *         Carry flag -  set if error
 * Registers changed: AX, CX
 *
 */
freeall:

	push	fs
	xor	cx,cx
	mov	fs,[first_mcb]		# start at first MCB
	call	checkmcb		# check that first MCB
	jc	9f

1:	cmp	dx,fs:[mcb_owner]	# found memory block of owner
	jne	2f
	mov	word ptr fs:[mcb_owner],0
	inc	cx
2:	call	nextmcb			# continue with next MCB
	jnc	1b
	or	ax,ax			# reached end of MCB list
	jz	8f
	stc
	jmp	9f

8:	call	cleanup			# cleanup free memory blocks
9:	pop	fs
	ret



/*
 **************************************************************************
 *
 * Find an MCB in the list.
 * Input:  DX  -  MCB to search for
 * Output: FS  -  segment of MCB
 *         AX  -  error code
 *         Carry flag  -  set if error
 * Registers changed: AX, ES
 *
 */
findmcb:

	mov	fs,[first_mcb]		# start at first MCB
	call	checkmcb		# check that first MCB
	jc	3f

1:	mov	ax,fs
	cmp	ax,dx			# compare MCB addresses
	je	3f			# found it
	call	nextmcb			# proceed with next MCB
	jnc	1b
	or	ax,ax			# found an error?
	jnz	2f
	mov	ax,DOS_ERR_INVMEM	# just got to end of list
2:	stc
3:	ret



/*
 **************************************************************************
 *
 * Cleanup MCB list by concatenating adjacent free MCBs.
 * Input:  none
 * Output: AX  -  error code
 *         Carry flag  -  set if error
 * Registers changed: AX
 *
 */
cleanup:

	push	fs			# we really have to save FS here
	push	bx
	push	dx
	mov	fs,[first_mcb]		# start at first MCB
	call	checkmcb		# check that first MCB
	jc	9f

1:	mov	ax,fs:[mcb_owner]
	or	ax,ax			# concatenation possible if current
	jz	2f			# MCB is free
5:	call	nextmcb			# otherwise check next MCB if its
	jnc	1b			# free
	jmp	4f			# handle error

2:	mov	dx,fs
6:	call	nextmcb			# get next MCB
	jnc	3f
4:	or	ax,ax			# at end of list? this 'or' also
	jz	9f			# clears the carry flag
	stc				# return with error
	jmp	9f

3:	mov	ax,fs:[mcb_owner]	# is next MCB free?
	or	ax,ax
	jnz	5b			# no, continue with next one
	mov	bl,fs:[mcb_id]
	mov	ax,fs:[mcb_size]
	inc	ax			# get total size of next block
	mov	fs,dx
	add	fs:[mcb_size],ax	# add size to preceding MCB
	mov	fs:[mcb_id],bl		# copy ID from old to new MCB
	jmp	6b			# continue with the next MCB

9:	pop	dx
	pop	bx
	pop	fs
	ret



/*
 **************************************************************************
 *
 * Proceed to next MCB.
 * Input:  FS  -  Segment of old MCB
 * Output: FS  -  Segment of new MCB
 *         AX  -  error code (0 if end of MCB list)
 *         Carry Flag  -  set if error
 * Registers changed: AX, ES
 *
 */
nextmcb:

	xor	ax,ax
	cmp	byte ptr fs:[mcb_id],MCB_END_ID
	je	8f
	mov	ax,fs
	add	ax,fs:[mcb_size]	# add size to current MCB pointer
	jc	7f			# should never happen
	inc	ax
	cmp	ax,[memory_end]		# check for upper limit
	jae	7f
	cmp	ax,[first_mcb]		# check for lower limit
	jb	7f
	mov	fs,ax
	xor	ax,ax			# fall through to MCB checking



/*
 **************************************************************************
 *
 * Check if MCB is correct.
 * Input:  ES  -  segment of MCB (register FS for 386+)
 * Output: AX  -  error code (unchanged if no error)
 *         Carry flag  -  set if error
 * Registers changed: AX
 *
 */
checkmcb:

	cmp	byte ptr fs:[mcb_id],MCB_MEM_ID
	je	9f
	cmp	byte ptr fs:[mcb_id],MCB_END_ID
	je	9f
7:	mov	ax,DOS_ERR_INVMCB
8:	stc				# error
9:	ret




/*
 **************************************************************************
 *
 */
	.end

