/* * Copyright (c) 2018-2019 Atmosphère-NX * * This program is free software; you can redistribute it and/or modify it * under the terms and conditions of the GNU General Public License, * version 2, as published by the Free Software Foundation. * * This program is distributed in the hope 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, see . */ #include #include "lmem_impl_exp_heap.hpp" namespace ams::lmem::impl { namespace { constexpr u16 FreeBlockMagic = 0x4652; /* FR */ constexpr u16 UsedBlockMagic = 0x5544; /* UD */ constexpr u16 DefaultGroupId = 0x00; constexpr u16 MaxGroupId = 0xFF; constexpr size_t MinimumAlignment = 4; constexpr size_t MaximumPaddingalignment = 0x80; constexpr AllocationMode DefaultAllocationMode = AllocationMode_FirstFit; constexpr size_t MinimumFreeBlockSize = 4; struct MemoryRegion { void *start; void *end; }; constexpr inline bool IsValidHeapHandle(HeapHandle handle) { return handle->magic == ExpHeapMagic; } constexpr inline ExpHeapHead *GetExpHeapHead(HeapHead *heap_head) { return &heap_head->impl_head.exp_heap_head; } constexpr inline const ExpHeapHead *GetExpHeapHead(const HeapHead *heap_head) { return &heap_head->impl_head.exp_heap_head; } constexpr inline HeapHead *GetHeapHead(ExpHeapHead *exp_heap_head) { return util::GetParentPointer<&HeapHead::impl_head>(util::GetParentPointer<&ImplementationHeapHead::exp_heap_head>(exp_heap_head)); } constexpr inline const HeapHead *GetHeapHead(const ExpHeapHead *exp_heap_head) { return util::GetParentPointer<&HeapHead::impl_head>(util::GetParentPointer<&ImplementationHeapHead::exp_heap_head>(exp_heap_head)); } constexpr inline void *GetExpHeapMemoryStart(ExpHeapHead *exp_heap_head) { return reinterpret_cast(reinterpret_cast(exp_heap_head) + sizeof(ImplementationHeapHead)); } constexpr inline void *GetMemoryBlockStart(ExpHeapMemoryBlockHead *head) { return reinterpret_cast(reinterpret_cast(head) + sizeof(*head)); } constexpr inline const void *GetMemoryBlockStart(const ExpHeapMemoryBlockHead *head) { return reinterpret_cast(reinterpret_cast(head) + sizeof(*head)); } constexpr inline void *GetMemoryBlockEnd(ExpHeapMemoryBlockHead *head) { return reinterpret_cast(reinterpret_cast(GetMemoryBlockStart(head)) + head->block_size); } constexpr inline const void *GetMemoryBlockEnd(const ExpHeapMemoryBlockHead *head) { return reinterpret_cast(reinterpret_cast(GetMemoryBlockStart(head)) + head->block_size); } constexpr inline ExpHeapMemoryBlockHead *GetHeadForMemoryBlock(const void *block) { return reinterpret_cast(reinterpret_cast(block) - sizeof(ExpHeapMemoryBlockHead)); } constexpr inline bool IsValidUsedMemoryBlock(const HeapHead *heap, const void *block) { /* Block must fall within the heap range. */ if (heap != nullptr) { if (block < heap->heap_start || heap->heap_end <= block) { return false; } } /* Block magic must be used. */ const ExpHeapMemoryBlockHead *head = GetHeadForMemoryBlock(block); if (head->magic != UsedBlockMagic) { return false; } /* End of block must remain within the heap range. */ if (heap != nullptr) { if (reinterpret_cast(block) + head->block_size > reinterpret_cast(heap->heap_end)) { return false; } } return true; } constexpr inline u16 GetMemoryBlockAlignmentPadding(const ExpHeapMemoryBlockHead *block_head) { return static_cast((block_head->attributes >> 8) & 0x7F); } inline void SetMemoryBlockAlignmentPadding(ExpHeapMemoryBlockHead *block_head, u16 padding) { block_head->attributes &= ~static_castattributes)>(0x7F << 8); block_head->attributes |= static_castattributes)>(padding & 0x7F) << 8; } constexpr inline u16 GetMemoryBlockGroupId(const ExpHeapMemoryBlockHead *block_head) { return static_cast(block_head->attributes & 0xFF); } inline void SetMemoryBlockGroupId(ExpHeapMemoryBlockHead *block_head, u16 group_id) { block_head->attributes &= ~static_castattributes)>(0xFF); block_head->attributes |= static_castattributes)>(group_id & 0xFF); } constexpr inline AllocationDirection GetMemoryBlockAllocationDirection(const ExpHeapMemoryBlockHead *block_head) { return static_cast((block_head->attributes >> 15) & 1); } inline void SetMemoryBlockAllocationDirection(ExpHeapMemoryBlockHead *block_head, AllocationDirection dir) { block_head->attributes &= ~static_castattributes)>(0x8000); block_head->attributes |= static_castattributes)>(dir) << 15; } inline void GetMemoryBlockRegion(MemoryRegion *out, ExpHeapMemoryBlockHead *head) { out->start = reinterpret_cast(reinterpret_cast(head) - GetMemoryBlockAlignmentPadding(head)); out->end = GetMemoryBlockEnd(head); } constexpr inline AllocationMode GetAllocationModeImpl(const ExpHeapHead *head) { return static_cast(head->mode); } inline void SetAllocationModeImpl(ExpHeapHead *head, AllocationMode mode) { head->mode = mode; } inline ExpHeapMemoryBlockHead *InitializeMemoryBlock(const MemoryRegion ®ion, u16 magic) { /* Construct the block. */ ExpHeapMemoryBlockHead *block = std::construct_at(reinterpret_cast(region.start)); /* Initialize all members. */ block->magic = magic; block->attributes = 0; block->block_size = GetPointerDifference(GetMemoryBlockStart(block), region.end); return block; } inline ExpHeapMemoryBlockHead *InitializeFreeMemoryBlock(const MemoryRegion ®ion) { return InitializeMemoryBlock(region, FreeBlockMagic); } inline ExpHeapMemoryBlockHead *InitializeUsedMemoryBlock(const MemoryRegion ®ion) { return InitializeMemoryBlock(region, UsedBlockMagic); } HeapHead *InitializeExpHeap(void *start, void *end, u32 option) { HeapHead *heap_head = reinterpret_cast(start); ExpHeapHead *exp_heap_head = GetExpHeapHead(heap_head); /* Initialize the parent heap. */ InitializeHeapHead(heap_head, ExpHeapMagic, GetExpHeapMemoryStart(exp_heap_head), end, option); /* Call exp heap member constructors. */ std::construct_at(std::addressof(exp_heap_head->free_list)); std::construct_at(std::addressof(exp_heap_head->used_list)); /* Set exp heap fields. */ exp_heap_head->group_id = DefaultGroupId; exp_heap_head->use_alignment_margins = false; SetAllocationModeImpl(exp_heap_head, DefaultAllocationMode); /* Initialize memory block. */ { MemoryRegion region{ .start = heap_head->heap_start, .end = heap_head->heap_end, }; exp_heap_head->free_list.push_back(*InitializeFreeMemoryBlock(region)); } return heap_head; } bool CoalesceFreedRegion(ExpHeapHead *head, const MemoryRegion *region) { auto prev_free_block_it = head->free_list.end(); MemoryRegion free_region = *region; /* Locate the block. */ for (auto it = head->free_list.begin(); it != head->free_list.end(); it++) { ExpHeapMemoryBlockHead *cur_free_block = &*it; if (cur_free_block < region->start) { prev_free_block_it = it; continue; } /* Coalesce block after, if possible. */ if (cur_free_block == region->end) { free_region.end = GetMemoryBlockEnd(cur_free_block); it = head->free_list.erase(it); /* Fill the memory with a pattern, for debug. */ FillUnallocatedMemory(GetHeapHead(head), cur_free_block, sizeof(ExpHeapMemoryBlockHead)); } break; } /* We'll want to insert after the previous free block. */ auto insertion_it = head->free_list.begin(); if (prev_free_block_it != head->free_list.end()) { /* There's a previous free block, so we want to insert as the next iterator. */ if (GetMemoryBlockEnd(&*prev_free_block_it) == region->start) { /* We can coalesce, so do so. */ free_region.start = &*prev_free_block_it; insertion_it = head->free_list.erase(prev_free_block_it); } else { /* We can't coalesce, so just select the next iterator. */ insertion_it = (++prev_free_block_it); } } /* Ensure region is big enough for a block. */ /* NOTE: Nintendo does not check against minimum block size here, only header size. */ /* We will check against minimum block size, to avoid the creation of zero-size blocks. */ if (GetPointerDifference(free_region.start, free_region.end) < sizeof(ExpHeapMemoryBlockHead) + MinimumFreeBlockSize) { return false; } /* Fill the memory with a pattern, for debug. */ FillFreedMemory(GetHeapHead(head), free_region.start, GetPointerDifference(free_region.start, free_region.end)); /* Insert the new memory block. */ head->free_list.insert(insertion_it, *InitializeFreeMemoryBlock(free_region)); return true; } void *ConvertFreeBlockToUsedBlock(ExpHeapHead *head, ExpHeapMemoryBlockHead *block_head, void *block, size_t size, AllocationDirection direction) { /* Calculate freed memory regions. */ MemoryRegion free_region_front; GetMemoryBlockRegion(&free_region_front, block_head); MemoryRegion free_region_back{ .start = reinterpret_cast(reinterpret_cast(block) + size), .end = free_region_front.end, }; /* Adjust end of head region. */ free_region_front.end = reinterpret_cast(reinterpret_cast(block) - sizeof(ExpHeapMemoryBlockHead)); /* Remove the old block. */ auto old_block_it = head->free_list.erase(head->free_list.iterator_to(*block_head)); /* If the front margins are big enough (and we're allowed to do so), make a new block. */ if ((GetPointerDifference(free_region_front.start, free_region_front.end) < sizeof(ExpHeapMemoryBlockHead) + MinimumFreeBlockSize) || (direction == AllocationDirection_Front && !head->use_alignment_margins && GetPointerDifference(free_region_front.start, free_region_front.end) < MaximumPaddingalignment)) { /* There isn't enough space for a new block, or else we're not allowed to make one. */ free_region_front.end = free_region_front.start; } else { /* Make a new block! */ head->free_list.insert(old_block_it, *InitializeFreeMemoryBlock(free_region_front)); } /* If the back margins are big enough (and we're allowed to do so), make a new block. */ if ((GetPointerDifference(free_region_back.start, free_region_back.end) < sizeof(ExpHeapMemoryBlockHead) + MinimumFreeBlockSize) || (direction == AllocationDirection_Back && !head->use_alignment_margins && GetPointerDifference(free_region_back.start, free_region_back.end) < MaximumPaddingalignment)) { /* There isn't enough space for a new block, or else we're not allowed to make one. */ free_region_back.end = free_region_back.start; } else { /* Make a new block! */ head->free_list.insert(old_block_it, *InitializeFreeMemoryBlock(free_region_back)); } /* Fill the memory with a pattern, for debug. */ FillAllocatedMemory(GetHeapHead(head), free_region_front.end, GetPointerDifference(free_region_front.end, free_region_back.start)); { /* Create the used block */ MemoryRegion used_region{ .start = reinterpret_cast(reinterpret_cast(block) - sizeof(ExpHeapMemoryBlockHead)), .end = free_region_back.start }; ExpHeapMemoryBlockHead *used_block = InitializeUsedMemoryBlock(used_region); /* Insert it into the used list. */ head->used_list.push_back(*used_block); SetMemoryBlockAllocationDirection(used_block, direction); SetMemoryBlockAlignmentPadding(used_block, static_cast(GetPointerDifference(free_region_front.end, used_block))); SetMemoryBlockGroupId(used_block, head->group_id); } return block; } void *AllocateFromHead(HeapHead *heap, size_t size, s32 alignment) { ExpHeapHead *exp_heap_head = GetExpHeapHead(heap); const bool is_first_fit = GetAllocationModeImpl(exp_heap_head) == AllocationMode_FirstFit; /* Choose a block. */ ExpHeapMemoryBlockHead *found_block_head = nullptr; void *found_block = nullptr; size_t best_size = std::numeric_limits::max(); for (auto it = exp_heap_head->free_list.begin(); it != exp_heap_head->free_list.end(); it++) { const uintptr_t absolute_block_start = reinterpret_cast(GetMemoryBlockStart(&*it)); const uintptr_t block_start = util::AlignUp(absolute_block_start, alignment); const size_t block_offset = block_start - absolute_block_start; if (it->block_size >= size + block_offset && best_size > it->block_size) { found_block_head = &*it; found_block = reinterpret_cast(block_start); best_size = it->block_size; if (is_first_fit || best_size == size) { break; } } } /* If we didn't find a block, return nullptr. */ if (found_block_head == nullptr) { return nullptr; } return ConvertFreeBlockToUsedBlock(exp_heap_head, found_block_head, found_block, size, AllocationDirection_Front); } void *AllocateFromTail(HeapHead *heap, size_t size, s32 alignment) { ExpHeapHead *exp_heap_head = GetExpHeapHead(heap); const bool is_first_fit = GetAllocationModeImpl(exp_heap_head) == AllocationMode_FirstFit; /* Choose a block. */ ExpHeapMemoryBlockHead *found_block_head = nullptr; void *found_block = nullptr; size_t best_size = std::numeric_limits::max(); for (auto it = exp_heap_head->free_list.rbegin(); it != exp_heap_head->free_list.rend(); it++) { const uintptr_t absolute_block_start = reinterpret_cast(GetMemoryBlockStart(&*it)); const uintptr_t block_start = util::AlignUp(absolute_block_start, alignment); const size_t block_offset = block_start - absolute_block_start; if (it->block_size >= size + block_offset && best_size > it->block_size) { found_block_head = &*it; found_block = reinterpret_cast(block_start); best_size = it->block_size; if (is_first_fit || best_size == size) { break; } } } /* If we didn't find a block, return nullptr. */ if (found_block_head == nullptr) { return nullptr; } return ConvertFreeBlockToUsedBlock(exp_heap_head, found_block_head, found_block, size, AllocationDirection_Back); } } HeapHandle CreateExpHeap(void *address, size_t size, u32 option) { const uintptr_t uptr_end = util::AlignDown(reinterpret_cast(address) + size, MinimumAlignment); const uintptr_t uptr_start = util::AlignUp(reinterpret_cast(address), MinimumAlignment); if (uptr_start > uptr_end || GetPointerDifference(uptr_start, uptr_end) < sizeof(ExpHeapMemoryBlockHead) + MinimumFreeBlockSize) { return nullptr; } return InitializeExpHeap(reinterpret_cast(uptr_start), reinterpret_cast(uptr_end), option); } void DestroyExpHeap(HeapHandle handle) { AMS_ASSERT(IsValidHeapHandle(handle)); FinalizeHeap(handle); } MemoryRange AdjustExpHeap(HeapHandle handle) { AMS_ASSERT(IsValidHeapHandle(handle)); HeapHead *heap_head = handle; ExpHeapHead *exp_heap_head = GetExpHeapHead(heap_head); /* If there's no free blocks, we can't do anything. */ if (exp_heap_head->free_list.empty()) { return MakeMemoryRange(handle->heap_end, 0); } /* Get the memory block end, make sure it really is the last block. */ ExpHeapMemoryBlockHead *block = &exp_heap_head->free_list.back(); void * const block_start = GetMemoryBlockStart(block); const size_t block_size = block->block_size; void * const block_end = reinterpret_cast(reinterpret_cast(block_start) + block_size); if (block_end != handle->heap_end) { return MakeMemoryRange(handle->heap_end, 0); } /* Remove the memory block. */ exp_heap_head->free_list.pop_back(); const size_t freed_size = block_size + sizeof(ExpHeapMemoryBlockHead); heap_head->heap_end = reinterpret_cast(reinterpret_cast(heap_head->heap_end) - freed_size); return MakeMemoryRange(heap_head->heap_end, freed_size); } void *AllocateFromExpHeap(HeapHandle handle, size_t size, s32 alignment) { AMS_ASSERT(IsValidHeapHandle(handle)); /* Fix up alignments less than 4. */ if (alignment == 1 || alignment == 2) { alignment = 4; } else if (alignment == -1 || alignment == -2) { alignment = -4; } /* Ensure the alignment is valid. */ const s32 abs_alignment = std::abs(alignment); AMS_ASSERT((abs_alignment & (abs_alignment - 1)) == 0); AMS_ASSERT(MinimumAlignment <= static_cast(abs_alignment)); /* Fix size to be correctly aligned. */ if (size == 0) { size = 1; } size = util::AlignUp(size, MinimumAlignment); /* Allocate a memory block. */ void *allocated_memory = nullptr; if (alignment >= 0) { allocated_memory = AllocateFromHead(handle, size, alignment); } else { allocated_memory = AllocateFromTail(handle, size, -alignment); } return allocated_memory; } void FreeToExpHeap(HeapHandle handle, void *mem_block) { /* Ensure this is actually a valid heap and a valid memory block we allocated. */ AMS_ASSERT(IsValidHeapHandle(handle)); AMS_ASSERT(IsValidUsedMemoryBlock(handle, mem_block)); /* TODO: Nintendo does not allow FreeToExpHeap(nullptr). Should we? */ /* Get block pointers. */ HeapHead *heap_head = handle; ExpHeapHead *exp_heap_head = GetExpHeapHead(heap_head); ExpHeapMemoryBlockHead *block = GetHeadForMemoryBlock(mem_block); MemoryRegion region; /* Erase the heap from the used list, and coalesce it with adjacent blocks. */ GetMemoryBlockRegion(®ion, block); exp_heap_head->used_list.erase(exp_heap_head->used_list.iterator_to(*block)); AMS_ASSERT(CoalesceFreedRegion(exp_heap_head, ®ion)); } size_t ResizeExpHeapMemoryBlock(HeapHandle handle, void *mem_block, size_t size) { /* Ensure this is actually a valid heap and a valid memory block we allocated. */ AMS_ASSERT(IsValidHeapHandle(handle)); AMS_ASSERT(IsValidUsedMemoryBlock(handle, mem_block)); ExpHeapHead *exp_heap_head = GetExpHeapHead(handle); ExpHeapMemoryBlockHead *block_head = GetHeadForMemoryBlock(mem_block); const size_t original_block_size = block_head->block_size; /* It's possible that there's no actual resizing being done. */ size = util::AlignUp(size, MinimumAlignment); if (size == original_block_size) { return size; } /* We're resizing one way or the other. */ if (size > original_block_size) { /* We want to try to make the block bigger. */ /* Find the free block after this one. */ void * const cur_block_end = GetMemoryBlockEnd(block_head); ExpHeapMemoryBlockHead *next_block_head = nullptr; for (auto it = exp_heap_head->free_list.begin(); it != exp_heap_head->free_list.end(); it++) { if (&*it == cur_block_end) { next_block_head = &*it; break; } } /* If we can't get a big enough allocation using the next block, give up. */ if (next_block_head == nullptr || size > original_block_size + sizeof(ExpHeapMemoryBlockHead) + next_block_head->block_size) { return 0; } /* Grow the block to encompass the next block. */ { /* Get block region. */ MemoryRegion new_free_region; GetMemoryBlockRegion(&new_free_region, next_block_head); /* Remove the next block from the free list. */ auto insertion_it = exp_heap_head->free_list.erase(exp_heap_head->free_list.iterator_to(*next_block_head)); /* Figure out the new block extents. */ void *old_start = new_free_region.start; new_free_region.start = reinterpret_cast(reinterpret_cast(mem_block) + size); /* Only maintain the new free region as a memory block candidate if it can hold a header. */ /* NOTE: Nintendo does not check against minimum block size here, only header size. */ /* We will check against minimum block size, to avoid the creation of zero-size blocks. */ if (GetPointerDifference(new_free_region.start, new_free_region.end) < sizeof(ExpHeapMemoryBlockHead) + MinimumFreeBlockSize) { new_free_region.start = new_free_region.end; } /* Adjust block sizes. */ block_head->block_size = GetPointerDifference(mem_block, new_free_region.start); if (GetPointerDifference(new_free_region.start, new_free_region.end) >= sizeof(ExpHeapMemoryBlockHead) + MinimumFreeBlockSize) { exp_heap_head->free_list.insert(insertion_it, *InitializeFreeMemoryBlock(new_free_region)); } /* Fill the memory with a pattern, for debug. */ FillAllocatedMemory(GetHeapHead(exp_heap_head), old_start, GetPointerDifference(old_start, new_free_region.start)); } } else { /* We're shrinking the block. Nice and easy. */ MemoryRegion new_free_region{ .start = reinterpret_cast(reinterpret_cast(mem_block)+ size), .end = GetMemoryBlockEnd(block_head) }; /* Try to free the new memory. */ block_head->block_size = size; if (!CoalesceFreedRegion(exp_heap_head, &new_free_region)) { /* We didn't shrink the block successfully, so restore the size. */ block_head->block_size = original_block_size; } } return block_head->block_size; } size_t GetExpHeapTotalFreeSize(HeapHandle handle) { AMS_ASSERT(IsValidHeapHandle(handle)); size_t total_size = 0; for (const auto &it : GetExpHeapHead(handle)->free_list) { total_size += it.block_size; } return total_size; } size_t GetExpHeapAllocatableSize(HeapHandle handle, s32 alignment) { AMS_ASSERT(IsValidHeapHandle(handle)); /* Ensure alignment is positive. */ alignment = std::abs(alignment); size_t max_size = std::numeric_limits::min(); size_t min_offset = std::numeric_limits::max(); for (const auto &it : GetExpHeapHead(handle)->free_list) { const uintptr_t absolute_block_start = reinterpret_cast(GetMemoryBlockStart(&it)); const uintptr_t block_start = util::AlignUp(absolute_block_start, alignment); const uintptr_t block_end = reinterpret_cast(GetMemoryBlockEnd(&it)); if (block_start < block_end) { const size_t block_size = GetPointerDifference(block_start, block_end); const size_t offset = GetPointerDifference(absolute_block_start, block_start); if (block_size > max_size || (block_size == max_size && offset < min_offset)) { max_size = block_size; min_offset = offset; } } } return max_size; } AllocationMode GetExpHeapAllocationMode(HeapHandle handle) { AMS_ASSERT(IsValidHeapHandle(handle)); return GetAllocationModeImpl(GetExpHeapHead(handle)); } AllocationMode SetExpHeapAllocationMode(HeapHandle handle, AllocationMode new_mode) { AMS_ASSERT(IsValidHeapHandle(handle)); ExpHeapHead *exp_heap_head = GetExpHeapHead(handle); const AllocationMode old_mode = GetAllocationModeImpl(exp_heap_head); SetAllocationModeImpl(exp_heap_head, new_mode); return old_mode; } u16 GetExpHeapGroupId(HeapHandle handle) { AMS_ASSERT(IsValidHeapHandle(handle)); return GetExpHeapHead(handle)->group_id; } u16 SetExpHeapGroupId(HeapHandle handle, u16 group_id) { AMS_ASSERT(IsValidHeapHandle(handle)); AMS_ASSERT(group_id <= MaxGroupId); ExpHeapHead *exp_heap_head = GetExpHeapHead(handle); const u16 old_group_id = exp_heap_head->group_id; exp_heap_head->group_id = group_id; return old_group_id; } void VisitExpHeapAllocatedBlocks(HeapHandle handle, HeapVisitor visitor, uintptr_t user_data) { AMS_ASSERT(IsValidHeapHandle(handle)); for (auto &it : GetExpHeapHead(handle)->used_list) { (*visitor)(GetMemoryBlockStart(&it), handle, user_data); } } size_t GetExpHeapMemoryBlockSize(const void *memory_block) { AMS_ASSERT(IsValidUsedMemoryBlock(nullptr, memory_block)); return GetHeadForMemoryBlock(memory_block)->block_size; } u16 GetExpHeapMemoryBlockGroupId(const void *memory_block) { AMS_ASSERT(IsValidUsedMemoryBlock(nullptr, memory_block)); return GetMemoryBlockGroupId(GetHeadForMemoryBlock(memory_block)); } AllocationDirection GetExpHeapMemoryBlockAllocationDirection(const void *memory_block) { AMS_ASSERT(IsValidUsedMemoryBlock(nullptr, memory_block)); return GetMemoryBlockAllocationDirection(GetHeadForMemoryBlock(memory_block)); } }