#include "memory.h" /* The 'heap' array is the global pool of memory from which pages are allocated and deallocated, used by every Bedrock instance. The 'heap_index' array tracks the owner of each page of memory: the value of each entry is the ID of the instance that owns / has allocated the corresponding page in the heap. An ID of zero indicates that the page is free / hasn't been allocated yet. If multiple instances are repeatedly allocating and deallocating pages, it's expected that the allocations will become very fragmented, with pages owned by each instance interleaving with one another. To find the heap address that corresponds with an allocated page address, the heap will need to be traversed, which makes random access very expensive. To mitigate this, the heap address of every page accessible from the current offset value (which determines the base page to address from) is cached in an array of 256 pointers, and the pointer corresponding with the current page is further cached as a pointer. The 'unallocated' array acts as a sort of null page, with every access of an unallocated page being routed instead to that page. This allows the read and write methods to omit a test to check whether a page is allocated or not; effectively, every page is always allocated. This is allowed because the Bedrock specification says that an unallocated read or write causes undefined behaviour, allowing us to optimise away the test. TODO: If one instance allocates memory, then a second instance allocates memory, then the first instance deallocates, then the second instance allocates some more, the pages allocated to the second instance will be out of order, but the code so far assumes that all pages are in the correct order (with gaps or not). There needs to be either a page reordering pass (time expensive) or an array to track the allocation order of pages for each instance (space expensive). The former approach would be the best, because it's unlikely that this would be a frequent occurrence. I'll need to shuffle pages around using memcpy and the null page. Only touch the pages of the current instance, to avoid having to recalculate the page caches of every instance. We can determine if pages will need to be shuffled / defragmented by checking if an unallocated page is traversed before the final allocated page is found. */ // Size of the heap in pages. The value was chosen by steadily increasing it // until the emulator refused to compile, and then easing back a bit. #define MAX_PAGES (12000) // The heap. static u8 heap[MAX_PAGES][256]; // the page heap. // Null page to use for unallocated reads and writes. static u8 unallocated[256]; // Map heap pages to instances. An ID of zero represents an unallocated page. static u8 heap_index[MAX_PAGES]; // Reset a read-write head on the memory device. void memory_head_reset(MemoryHead *head) { head->offset = 0; head->byte = 0; head->point = (u8*) &unallocated; head->page = 0; for (int i=0; i<256; i++) head->cache[i] = (u8*) &unallocated; } // Reset the memory device. void memory_reset(MemoryDevice *mem) { mem->count = 0; mem->copy = 0; memory_allocate(mem); memory_head_reset(&mem->head1); memory_head_reset(&mem->head2); } // Read a byte from a memory head. u8 memory_read(MemoryHead *head) { u8 output = head->point[head->byte++]; if (head->byte == 0) { head->page++; memory_refresh_page(head); } return output; } // Write a byte to a memory head. void memory_write(MemoryHead *head, u8 value) { head->point[head->byte++] = value; if (head->byte == 0) { head->page++; memory_refresh_page(head); } } void memory_refresh_page(MemoryHead *head) { head->point = head->cache[head->page]; } // Recalculate the pointers stored in the page cache. void memory_refresh_cache(MemoryDevice *mem, MemoryHead *head) { // Point every page in the cache to the unallocated page. for (int i=0; i<256; i++) head->cache[i] = (u8*) &unallocated; // Iterate over every page in the heap, gather all pages allocated to // this instance that belong to the current offset. int n = 0; int cache_i = 0; for (int heap_i=0; heap_iid) { if (n++ >= head->offset) { head->cache[cache_i++] = (u8*) &heap[heap_i]; if (cache_i == 256) break; } } } } // Allocate mem->count pages of memory. void memory_allocate(MemoryDevice *mem) { // Track how many pages have been allocated/deallocated so far. int n = 0; // Allocate memory. if (mem->count > mem->allocated) { int to_allocate = mem->count - mem->allocated; for (int i=0; iid; if (++n == to_allocate) break; } } // Deallocate memory. } else if (mem->count < mem->allocated) { for (int i=0; iid) { if (++n > mem->count) { // Clear the page when it's deallocated. memset(heap[i], 0, 256); heap_index[i] = 0; } } } } // Re-count the number of pages allocated to this instance. mem->allocated = 0; for (int i=0; iid) mem->allocated++; } // Refresh the page caches for both heads. memory_refresh_cache(mem, &mem->head1); memory_refresh_cache(mem, &mem->head2); } // Copy mem->count pages from head 2 to head 1. void memory_copy(MemoryDevice *mem) { if (mem->head1.offset == mem->head2.offset) return; for (int n=0; ncopy; n++) { if (mem->head1.cache[n] == (u8*) &unallocated) return; if (mem->head2.cache[n] == (u8*) &unallocated) return; memcpy(mem->head1.cache[n], mem->head2.cache[n], 256); } }