using BizHawk.Emulation.Common; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using System.Runtime.InteropServices; using BizHawk.Common.BizInvoke; 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 const MemoryBlock.Protection FREE = (MemoryBlock.Protection)255; private readonly MemoryBlock.Protection[] _pages; private readonly byte[] _pagesAsBytes; public MapHeap(ulong start, ulong size, string name) { size = WaterboxUtils.AlignUp(size); Memory = new MemoryBlock(start, size); Name = name; _pagesAsBytes = new byte[size >> WaterboxUtils.PageShift]; _pages = (MemoryBlock.Protection[])(object)_pagesAsBytes; for (var i = 0; i < _pages.Length; i++) _pages[i] = FREE; Console.WriteLine("Created mapheap `{1}` at {0:x16}:{2:x16}", start, name, start + size); } // find consecutive unused pages to map private int FindConsecutiveFreePages(int count) { return FindConsecutiveFreePagesAssumingFreed(count, -1, -1); } // find consecutive unused pages to map, pretending that [startPage..startPage + numPages) is free // used in realloc private int FindConsecutiveFreePagesAssumingFreed(int count, int startPage, int numPages) { var starts = new List(); var sizes = new List(); var currStart = 0; for (var i = 0; i <= _pages.Length; i++) { if (i == _pages.Length || _pages[i] != FREE && (i < startPage || i >= startPage + numPages)) { if (currStart < i) { starts.Add(currStart); var size = i - currStart; if (size == count) return currStart; sizes.Add(i - currStart); } currStart = i + 1; } } int bestIdx = -1; int bestSize = int.MaxValue; for (int i = 0; i < sizes.Count; i++) { if (sizes[i] < bestSize && sizes[i] >= count) { bestSize = sizes[i]; bestIdx = i; } } if (bestIdx != -1) return starts[bestIdx]; else return -1; } private void ProtectInternal(int startPage, int numPages, MemoryBlock.Protection prot, bool wasUsed) { for (var i = startPage; i < startPage + numPages; i++) _pages[i] = prot; ulong start = GetStartAddr(startPage); ulong length = ((ulong)numPages) << WaterboxUtils.PageShift; if (prot == FREE) { Memory.Protect(start, length, MemoryBlock.Protection.RW); WaterboxUtils.ZeroMemory(Z.US(start), (long)length); Memory.Protect(start, length, MemoryBlock.Protection.None); Used -= length; Console.WriteLine($"Freed {length} bytes on {Name}, utilization {Used}/{Memory.Size} ({100.0 * Used / Memory.Size:0.#}%)"); } else { Memory.Protect(start, length, prot); if (wasUsed) { Console.WriteLine($"Set protection for {length} bytes on {Name} to {prot}"); } else { Used += length; Console.WriteLine($"Allocated {length} bytes on {Name}, utilization {Used}/{Memory.Size} ({100.0 * Used / Memory.Size:0.#}%)"); } } } private void RefreshProtections(int startPage, int pageCount) { int ps = startPage; for (int i = startPage; i < startPage + pageCount; i++) { if (i == startPage + pageCount - 1 || _pages[i] != _pages[i + 1]) { var p = _pages[i]; ulong zstart = GetStartAddr(ps); ulong zlength = (ulong)(i - ps + 1) << WaterboxUtils.PageShift; Memory.Protect(zstart, zlength, p == FREE ? MemoryBlock.Protection.None : p); ps = i + 1; } } } private void RefreshAllProtections() { RefreshProtections(0, _pages.Length); } private bool EnsureMapped(int startPage, int pageCount) { for (int i = startPage; i < startPage + pageCount; i++) if (_pages[i] == FREE) return false; return true; } public ulong Map(ulong size, MemoryBlock.Protection prot) { if (size == 0) return 0; int numPages = WaterboxUtils.PagesNeeded(size); int startPage = FindConsecutiveFreePages(numPages); if (startPage == -1) return 0; var ret = GetStartAddr(startPage); ProtectInternal(startPage, numPages, prot, false); 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 || oldSize == 0 || newSize == 0) return 0; var oldStartPage = GetPage(start); var oldNumPages = WaterboxUtils.PagesNeeded(oldSize); if (!EnsureMapped(oldStartPage, oldNumPages)) return 0; var oldProt = _pages[oldStartPage]; int newNumPages = WaterboxUtils.PagesNeeded(newSize); if (!canMove) { if (newNumPages <= oldNumPages) { if (newNumPages < oldNumPages) ProtectInternal(oldStartPage + newNumPages, oldNumPages - newNumPages, FREE, true); return start; } else if (newNumPages > oldNumPages) { for (var i = oldStartPage + oldNumPages; i < oldStartPage + newNumPages; i++) if (_pages[i] != FREE) return 0; ProtectInternal(oldStartPage + oldNumPages, newNumPages - oldNumPages, oldProt, false); return start; } } // if moving is allowed, we always move to simplify and defragment when possible int newStartPage = FindConsecutiveFreePagesAssumingFreed(newNumPages, oldStartPage, oldNumPages); if (newStartPage == -1) return 0; var copyDataLen = Math.Min(oldSize, newSize); var copyPageLen = Math.Min(oldNumPages, newNumPages); var data = new byte[copyDataLen]; Memory.Protect(start, copyDataLen, MemoryBlock.Protection.RW); Marshal.Copy(Z.US(start), data, 0, (int)copyDataLen); var pages = new MemoryBlock.Protection[copyPageLen]; Array.Copy(_pages, oldStartPage, pages, 0, copyPageLen); ProtectInternal(oldStartPage, oldNumPages, FREE, true); ProtectInternal(newStartPage, newNumPages, MemoryBlock.Protection.RW, false); var ret = GetStartAddr(newStartPage); Marshal.Copy(data, 0, Z.US(ret), (int)copyDataLen); Array.Copy(pages, 0, _pages, newStartPage, copyPageLen); RefreshProtections(newStartPage, copyPageLen); if (newNumPages > oldNumPages) ProtectInternal(newStartPage + oldNumPages, newNumPages - oldNumPages, oldProt, true); return ret; } public bool Unmap(ulong start, ulong size) { return Protect(start, size, FREE); } public bool Protect(ulong start, ulong size, MemoryBlock.Protection prot) { if (start < Memory.Start || start + size > Memory.End || size == 0) return false; var startPage = GetPage(start); var numPages = WaterboxUtils.PagesNeeded(size); if (!EnsureMapped(startPage, numPages)) return false; ProtectInternal(startPage, numPages, prot, true); return true; } public void Dispose() { if (Memory != null) { Memory.Dispose(); Memory = null; } } private const ulong MAGIC = 0x1590abbcdeef5910; public void SaveStateBinary(BinaryWriter bw) { bw.Write(Name); bw.Write(Memory.Size); bw.Write(Used); bw.Write(Memory.XorHash); bw.Write(_pagesAsBytes); Memory.Protect(Memory.Start, Memory.Size, MemoryBlock.Protection.R); var srcs = Memory.GetXorStream(Memory.Start, Memory.Size, false); for (int i = 0, addr = 0; i < _pages.Length; i++, addr += WaterboxUtils.PageSize) { if (_pages[i] != FREE) { srcs.Seek(addr, SeekOrigin.Begin); WaterboxUtils.CopySome(srcs, bw.BaseStream, WaterboxUtils.PageSize); } } bw.Write(MAGIC); RefreshAllProtections(); } 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)); if (br.BaseStream.Read(_pagesAsBytes, 0, _pagesAsBytes.Length) != _pagesAsBytes.Length) throw new InvalidOperationException("Unexpected error reading!"); Used = 0; Memory.Protect(Memory.Start, Memory.Size, MemoryBlock.Protection.RW); var dsts = Memory.GetXorStream(Memory.Start, Memory.Size, true); for (int i = 0, addr = 0; i < _pages.Length; i++, addr += WaterboxUtils.PageSize) { if (_pages[i] != FREE) { dsts.Seek(addr, SeekOrigin.Begin); WaterboxUtils.CopySome(br.BaseStream, dsts, WaterboxUtils.PageSize); Used += (uint)WaterboxUtils.PageSize; } } if (Used != used) throw new InvalidOperationException("Internal savestate error"); if (br.ReadUInt64() != MAGIC) throw new InvalidOperationException("Savestate internal error"); RefreshAllProtections(); } public static void StressTest() { var allocs = new Dictionary(); var mmo = new MapHeap(0x36a00000000, 256 * 1024 * 1024, "ballsacks"); var rnd = new Random(12512); for (int i = 0; i < 40; i++) { ulong siz = (ulong)(rnd.Next(256 * 1024) + 384 * 1024); siz = siz / 4096 * 4096; var ptr = mmo.Map(siz, MemoryBlock.Protection.RW); allocs.Add(ptr, siz); } for (int i = 0; i < 20; i++) { int idx = rnd.Next(allocs.Count); var elt = allocs.ElementAt(idx); mmo.Unmap(elt.Key, elt.Value); allocs.Remove(elt.Key); } for (int i = 0; i < 40; i++) { ulong siz = (ulong)(rnd.Next(256 * 1024) + 384 * 1024); siz = siz / 4096 * 4096; var ptr = mmo.Map(siz, MemoryBlock.Protection.RW); allocs.Add(ptr, siz); } for (int i = 0; i < 20; i++) { int idx = rnd.Next(allocs.Count); var elt = allocs.ElementAt(idx); mmo.Unmap(elt.Key, elt.Value); } } } }