using BizHawk.Emulation.Common; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; namespace BizHawk.Emulation.Cores.Waterbox { /// /// a heap that supports basic alloc, free, and realloc calls /// internal sealed class MapHeap : IBinaryStateable, IDisposable { public MemoryBlock Memory { get; private set; } /// /// name, used in identifying errors /// public string Name { get; private set; } /// /// total number of bytes allocated /// public ulong Used { get; private set; } /// /// get a page index within the block /// private int GetPage(ulong addr) { return (int)((addr - Memory.Start) >> WaterboxUtils.PageShift); } /// /// get a start address for a page index within the block /// private ulong GetStartAddr(int page) { return ((ulong)page << WaterboxUtils.PageShift) + Memory.Start; } private class Bin { /// /// first page# in this bin, inclusive /// public int StartPage; /// /// numbe of pages in this bin /// public int PageCount; /// /// first page# not in this bin /// public int EndPage => StartPage + PageCount; public MemoryBlock.Protection Protection; /// /// true if not mapped (we distinguish between PROT_NONE and not mapped) /// public bool Free { get { return (byte)Protection == 255; } set { Protection = value ? (MemoryBlock.Protection)255 : MemoryBlock.Protection.None; } } public Bin Next; /// /// split this bin, keeping only numPages pages /// public bool Cleave(int numPages) { int nextPages = PageCount - numPages; if (nextPages > 0) { Next = new Bin { StartPage = StartPage + numPages, PageCount = nextPages, Next = Next }; PageCount = numPages; return true; } else { return false; } } /// /// activate the protection specified by this block /// public void ApplyProtection(MemoryBlock m) { var prot = Free ? MemoryBlock.Protection.None : Protection; var start = ((ulong)StartPage << WaterboxUtils.PageShift) + m.Start; var length = (ulong)PageCount << WaterboxUtils.PageShift; m.Protect(start, length, prot); } } private Bin _root; public MapHeap(ulong start, ulong size, string name) { size = WaterboxUtils.AlignUp(size); Memory = new MemoryBlock(start, size); Name = name; Console.WriteLine("Created mapheap `{1}` at {0:x16}:{2:x16}", start, name, start + size); _root = new Bin { StartPage = 0, PageCount = (int)(size >> WaterboxUtils.PageShift), Free = true }; } /// /// gets the bin that contains a page /// private Bin GetBinForStartPage(int page) { Bin curr = _root; while (curr.StartPage + curr.PageCount <= page) curr = curr.Next; return curr; } /// /// gets the bin that contains the page before the passed page, returning null if /// any bin along the way is Free /// private Bin GetBinForEndPageEnsureAllocated(int page, Bin start) { Bin curr = start; while (curr != null && curr.StartPage + curr.PageCount < page) { if (curr.Free) return null; curr = curr.Next; } return curr; } public ulong Map(ulong size, MemoryBlock.Protection prot) { int numPages = WaterboxUtils.PagesNeeded(size); Bin best = null; Bin curr = _root; // find smallest potential bin do { if (curr.Free && curr.PageCount >= numPages) { if (best == null || curr.PageCount < best.PageCount) { best = curr; if (curr.PageCount == numPages) break; } } curr = curr.Next; } while (curr != null); if (best == null) return 0; if (best.Cleave(numPages)) best.Next.Free = true; best.Protection = prot; var ret = GetStartAddr(best.StartPage); var totalSize = ((ulong)numPages) << WaterboxUtils.PageShift; Memory.Protect(ret, totalSize, prot); Used += totalSize; Console.WriteLine($"Allocated {totalSize} bytes on {Name}, utilization {Used}/{Memory.Size} ({100.0 * Used / Memory.Size:0.#}%)"); return ret; } public ulong Remap(ulong start, ulong oldSize, ulong newSize, bool canMove) { // TODO: what is the expected behavior when everything requested for remap is allocated, // but with different protections? if (start < Memory.Start || start + oldSize > Memory.End) return 0; var oldStartPage = GetPage(start); var oldStartBin = GetBinForStartPage(oldStartPage); if (oldSize == 0 && canMove) { if (oldStartBin.Free) return 0; else return Map(newSize, oldStartBin.Protection); } var oldNumPages = WaterboxUtils.PagesNeeded(oldSize); var oldEndPage = oldStartPage + oldNumPages; // first, check if the requested area is actually mapped var oldEndBin = GetBinForEndPageEnsureAllocated(oldEndPage, oldStartBin); if (oldEndBin == null) return 0; var newNumPages = WaterboxUtils.PagesNeeded(newSize); var newEndPage = oldStartPage + newNumPages; if (newEndPage > oldEndPage) { // increase size // the only way this will work in place is if all of the remaining space is free Bin nextBin; if (oldEndBin.EndPage == oldEndPage // if end bin is too bag, space after that is used by something else && (nextBin = oldEndBin.Next) != null // can't go off the edge && nextBin.Free && nextBin.EndPage >= newEndPage) { nextBin.Protection = oldStartBin.Protection; if (nextBin.Cleave(newEndPage - nextBin.StartPage)) nextBin.Next.Free = true; nextBin.ApplyProtection(Memory); var oldTotalSize = ((ulong)oldNumPages) << WaterboxUtils.PageShift; var newTotalSize = ((ulong)newNumPages) << WaterboxUtils.PageShift; Used += newTotalSize; Used -= oldTotalSize; Console.WriteLine($"Reallocated from {oldTotalSize} bytes to {newTotalSize} bytes on {Name}, utilization {Used}/{Memory.Size} ({100.0 * Used / Memory.Size:0.#}%)"); return start; } // could not increase in place, so move if (!canMove) return 0; // if there's some free space right before `start`, and some right after, but not enough // to extend in place, it's possible that a realloc would succeed reusing the same space, // but would fail anywhere else due to heavy memory pressure. // that would be a much more complicated algorithm; we'd need to compute a new allocation // as if this one had been freed, but still be able to preserve this if that allocation // still failed. instead, we ignore this case. var ret = Map(newSize, oldStartBin.Protection); if (ret != 0) { // move data // NB: oldSize > 0 Memory.Protect(start, oldSize, MemoryBlock.Protection.R); var ss = Memory.GetStream(start, oldSize, false); Memory.Protect(ret, oldSize, MemoryBlock.Protection.RW); var ds = Memory.GetStream(ret, oldSize, true); ss.CopyTo(ds); Memory.Protect(ret, oldSize, oldStartBin.Protection); UnmapPagesInternal(oldStartPage, oldNumPages, oldStartBin); return ret; } else { return 0; } } else if (newEndPage < oldEndPage) { // shrink in place var s = GetBinForStartPage(newEndPage); UnmapPagesInternal(newEndPage, oldEndPage - newEndPage, s); return start; } else { // no change return start; } } public bool Unmap(ulong start, ulong size) { if (start < Memory.Start || start + size > Memory.End) return false; if (size == 0) return true; var startPage = GetPage(start); var numPages = WaterboxUtils.PagesNeeded(size); var endPage = startPage + numPages; // check to see if the requested area is actually mapped var startBin = GetBinForStartPage(startPage); if (GetBinForEndPageEnsureAllocated(endPage, startBin) == null) return false; UnmapPagesInternal(startPage, numPages, startBin); return true; } /// /// frees some pages. assumes they are all allocated /// private void UnmapPagesInternal(int startPage, int numPages, Bin startBin) { // from the various paths we took to get here, we must be unmapping at least one page var endPage = startPage + numPages; Bin freeBin = startBin; if (!freeBin.Free && freeBin.StartPage != startPage) { freeBin.Cleave(startPage - freeBin.StartPage); freeBin = freeBin.Next; freeBin.Free = true; } MemoryBlock.Protection lastEaten = MemoryBlock.Protection.None; while (freeBin.EndPage < endPage) { freeBin.PageCount += freeBin.Next.PageCount; lastEaten = freeBin.Next.Protection; freeBin.Next = freeBin.Next.Next; } if (freeBin.Cleave(freeBin.EndPage - endPage)) { freeBin.Next.Protection = lastEaten; } freeBin.ApplyProtection(Memory); var totalSize = ((ulong)numPages) << WaterboxUtils.PageShift; Used -= totalSize; Console.WriteLine($"Freed {totalSize} bytes on {Name}, utilization {Used}/{Memory.Size} ({100.0 * Used / Memory.Size:0.#}%)"); } public void Dispose() { if (Memory != null) { Memory.Dispose(); Memory = null; } } public void SaveStateBinary(BinaryWriter bw) { bw.Write(Name); bw.Write(Memory.Size); bw.Write(Used); bw.Write(Memory.XorHash); var bin = _root; do { bw.Write(bin.PageCount); bw.Write((byte)bin.Protection); if (!bin.Free) { var start = GetStartAddr(bin.StartPage); var length = (ulong)bin.PageCount << WaterboxUtils.PageShift; if (bin.Protection == MemoryBlock.Protection.None) Memory.Protect(start, length, MemoryBlock.Protection.R); Memory.GetXorStream(start, length, false).CopyTo(bw.BaseStream); if (bin.Protection == MemoryBlock.Protection.None) Memory.Protect(start, length, MemoryBlock.Protection.None); } bin = bin.Next; } while (bin != null); bw.Write(-1); } public void LoadStateBinary(BinaryReader br) { var name = br.ReadString(); if (name != Name) throw new InvalidOperationException(string.Format("Name did not match for mapheap {0}", Name)); var size = br.ReadUInt64(); if (size != Memory.Size) throw new InvalidOperationException(string.Format("Size did not match for mapheap {0}", Name)); var used = br.ReadUInt64(); var hash = br.ReadBytes(Memory.XorHash.Length); if (!hash.SequenceEqual(Memory.XorHash)) throw new InvalidOperationException(string.Format("Hash did not match for mapheap {0}. Is this the same rom?", Name)); Used = 0; int startPage = 0; int pageCount; Bin scratch = new Bin(), curr = scratch; while ((pageCount = br.ReadInt32()) != -1) { var next = new Bin { StartPage = startPage, PageCount = pageCount, Protection = (MemoryBlock.Protection)br.ReadByte() }; startPage += pageCount; if (!next.Free) { var start = GetStartAddr(next.StartPage); var length = (ulong)pageCount << WaterboxUtils.PageShift; Memory.Protect(start, length, MemoryBlock.Protection.RW); WaterboxUtils.CopySome(br.BaseStream, Memory.GetXorStream(start, length, true), (long)length); Used += length; } next.ApplyProtection(Memory); curr.Next = next; curr = next; } if (used != Used) throw new InvalidOperationException(string.Format("Inernal error loading mapheap {0}", Name)); _root = scratch.Next; } } }