# Custom first-fit malloc routines by Lameguy64
# Part of the PSn00bSDK Project

.set noreorder

.set ND_PREV,	0	# Address to previous block (NULL if starting block)
.set ND_NEXT,	4	# Address to next block (NULL if end block)
.set ND_SIZE,	8	# Size of block
.set ND_HSIZ,	12

.section .text


# Stupid small function just to get bss end
# due to GCC insisting externs to be gp relative
.global GetBSSend
.type GetBSSend, @function
GetBSSend:
	la		$v0, _end
	jr		$ra
	nop
	

# Initializes the heap for malloc
#   a0 - Starting address of heap
#   a1 - Size of memory heap
#
.global InitHeap
.type InitHeap, @function
InitHeap:
	la		$v0, _malloc_addr
	sw		$a0, 0($v0)
	la		$v0, _malloc_size
	sw		$a1, 0($v0)
	
	sw		$0 , ND_PREV($a0)		# Set heap header
	sw		$0 , ND_NEXT($a0)
	jr		$ra
	sw		$0 , ND_SIZE($a0)
	
	
# Changes the heap size without clearing or relocating the heap
#   a0 - Size of memory heap in bytes
.global SetHeapSize
.type SetHeapSize, @function
SetHeapSize:
	la		$v1, _malloc_size
	lw		$v0, 0($v1)
	jr		$ra
	sw		$a1, 0($v1)


# Allocates a block of memory in the heap
#   a0 - Size of memory block to allocate.
#
.global malloc
.type malloc, @function
malloc:
	addiu	$a0, 3					# Round size to a multiple of 4
	srl		$a0, 2
	
	la		$a2, _malloc_addr
	lw		$a2, 0($a2)
	sll		$a0, 2
	
.Lfind_next:
	
	move	$a1, $a2
	
	lw		$a2, ND_NEXT($a1)		# Get block header
	lw		$v1, ND_SIZE($a1)
	
	subu	$v0, $a2, $a1			# Compute space between current and next
	
	beqz	$v1, .Lempty_block		# Occupy empty block (if size = 0)
	nop
	
	beqz	$a2, .Lnew_block		# Allocate a new block (if no next)
	nop
	
	addiu	$v0, -(ND_HSIZ*2)		# Compute remaining space of block
	subu	$v0, $v1
	
	blt		$v0, $a0, .Lfind_next	# Search for the next block if space is not big enough
	nop
	
	# Perform a block split using remaining space of current block

	addiu	$v0, $a1, ND_HSIZ		# Compute address for new header
	addu	$v0, $v1
	
	sw		$a1, ND_PREV($v0)		# Set the new block header
	sw		$a2, ND_NEXT($v0)
	sw		$a0, ND_SIZE($v0)
	
	sw		$v0, ND_NEXT($a1)		# Update previous and next blocks
	sw		$v0, ND_PREV($a2)
	
	jr		$ra
	addiu	$v0, ND_HSIZ
	
.Lempty_block:						# Occupy an empty block

	beqz	$a2, .Lno_next			# Skip size calculation if there's no next
	nop
	
	addiu	$v0, -ND_HSIZ
	blt		$v0, $a0, .Lfind_next
	nop
	
	b		.Lskip_space_check
	nop
	
.Lno_next:

	la		$v1, _malloc_addr		# Check if there's enough space for a block
	lw		$v1, 0($v1)
	la		$v0, _malloc_size
	lw		$v0, 0($v0)
	
	subu	$v1, $a1, $v1
	addu	$v1, $a0
	addiu	$v1, ND_HSIZ
	
	bgt		$v1, $v0, .Lno_space
	nop
	
.Lskip_space_check:

	sw		$a0, ND_SIZE($a1)
	jr		$ra						# Return address
	addiu	$v0, $a1, ND_HSIZ
	
.Lnew_block:						# Create a new block

	addu	$a2, $a1, $v1			# Compute address for new block
	addiu	$a2, ND_HSIZ
	
	la		$v1, _malloc_addr
	lw		$v1, 0($v1)
	la		$v0, _malloc_size
	lw		$v0, 0($v0)
	
	subu	$v1, $a2, $v1
	addu	$v1, $a0
	addiu	$v1, ND_HSIZ
	
	bgt		$v1, $v0, .Lno_space	# Reject if it exceeds specified size
	nop
	
	sw		$a1, ND_PREV($a2)
	sw		$0 , ND_NEXT($a2)
	sw		$a0, ND_SIZE($a2)
	
	sw		$a2, ND_NEXT($a1)
	
	jr		$ra						# Return address
	addiu	$v0, $a2, ND_HSIZ
	
.Lno_space:							# Return a null if no space can be found

	jr		$ra
	move	$v0, $0

	
# Allocates a block of memory in block units and zero fills the
# allocated block.
#   a0 - Block size.
#	a1 - Number of blocks to allocate
#
.global calloc
.type calloc, @function
calloc:
	mult	$a0, $a1
	addiu	$sp, -4
	sw		$ra, 0($sp)
	
	jal		malloc
	mflo	$a0
	
	move	$a0, $v0
	mflo	$a1

.Lclear_loop:

	sw		$0 , 0($a0)
	addi	$a1, 4
	bgtz	$a1, .Lclear_loop
	addiu	$a0, 4
	
	lw		$ra, 0($sp)
	addiu	$sp, 4
	jr		$ra
	nop

	
# Deallocates an allocated block
#	a0 - An address returned by malloc to deallocate
#
.global free
.type free, @function
free:

	addiu	$a0, -ND_HSIZ
	lw		$a1, ND_PREV($a0)
	lw		$a2, ND_NEXT($a0)
	
	beqz	$a1, .Lis_start			# Check if block is a starting block
	nop
	
	beqz	$a2, .Lis_end
	nop

	# Unlink
	
	sw		$a2, ND_NEXT($a1)
	jr		$ra
	sw		$a1, ND_PREV($a2)
	
.Lis_end:							# Unlinks the ending block

	jr		$ra
	sw		$0 , ND_NEXT($a1)
	
.Lis_start:							# Simply set size to 0 if starting block

	jr		$ra
	sw		$0 , ND_SIZE($a0)


# Internal variables
.comm _malloc_addr, 4, 4
.comm _malloc_size, 4, 4
