367 lines
11 KiB
C#
367 lines
11 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// a heap that supports basic alloc, free, and realloc calls
|
|
/// </summary>
|
|
internal sealed class MapHeap : IBinaryStateable, IDisposable
|
|
{
|
|
public MemoryBlock Memory { get; private set; }
|
|
/// <summary>
|
|
/// name, used in identifying errors
|
|
/// </summary>
|
|
public string Name { get; private set; }
|
|
|
|
/// <summary>
|
|
/// total number of bytes allocated
|
|
/// </summary>
|
|
public ulong Used { get; private set; }
|
|
|
|
/// <summary>
|
|
/// get a page index within the block
|
|
/// </summary>
|
|
private int GetPage(ulong addr)
|
|
{
|
|
return (int)((addr - Memory.Start) >> WaterboxUtils.PageShift);
|
|
}
|
|
|
|
/// <summary>
|
|
/// get a start address for a page index within the block
|
|
/// </summary>
|
|
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 {nameof(MapHeap)} `{name}` at {start:x16}:{start + size:x16}");
|
|
}
|
|
|
|
// 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<int>();
|
|
var sizes = new List<int>();
|
|
|
|
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($"Name did not match for {nameof(MapHeap)} {Name}");
|
|
var size = br.ReadUInt64();
|
|
if (size != Memory.Size)
|
|
throw new InvalidOperationException($"Size did not match for {nameof(MapHeap)} {Name}");
|
|
var used = br.ReadUInt64();
|
|
var hash = br.ReadBytes(Memory.XorHash.Length);
|
|
if (!hash.SequenceEqual(Memory.XorHash))
|
|
throw new InvalidOperationException($"Hash did not match for {nameof(MapHeap)} {Name}. Is this the same rom?");
|
|
|
|
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<ulong, ulong>();
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|