diff --git a/src/BizHawk.Client.Common/Api/Classes/MemoryApi.cs b/src/BizHawk.Client.Common/Api/Classes/MemoryApi.cs index 9683945b313..05154330562 100644 --- a/src/BizHawk.Client.Common/Api/Classes/MemoryApi.cs +++ b/src/BizHawk.Client.Common/Api/Classes/MemoryApi.cs @@ -264,7 +264,7 @@ public IReadOnlyList ReadByteRange(long addr, int length, string domain = return newBytes; } - public void WriteByteRange(long addr, IReadOnlyList memoryblock, string domain = null) + public void WriteByteRange(long addr, Span memoryblock, string domain = null) { var d = NamedDomainOrCurrent(domain); if (!d.Writable) @@ -273,15 +273,83 @@ public void WriteByteRange(long addr, IReadOnlyList memoryblock, string do return; } if (addr < 0) LogCallback($"Warning: Attempted writes on addresses {addr}..-1 outside range of domain {d.Name} in {nameof(WriteByteRange)}()"); - var lastReqAddr = addr + memoryblock.Count - 1; - var indexAfterLast = Math.Min(Math.Max(-1L, lastReqAddr), d.Size - 1L) + 1L; var iDst = Math.Min(Math.Max(0L, addr), d.Size); var iSrc = checked((int) (iDst - addr)); - using (d.EnterExit()) + var lastReqAddrExclusive = addr + memoryblock.Length; + var lastAddressExclusive = Math.Min(Math.Max(0, lastReqAddrExclusive), d.Size); + var length = lastAddressExclusive - iDst; + if (length > 0) using (d.EnterExit()) d.BulkPokeByte(iDst, memoryblock.Slice(start: iSrc, length: (int)length)); + if (lastReqAddrExclusive > d.Size) LogCallback($"Warning: Attempted writes on addresses {d.Size}..{lastReqAddrExclusive} outside range of domain {d.Name} in {nameof(WriteByteRange)}()"); + } + + public IReadOnlyList ReadU16Range(long addr, int count, string domain = null) + { + var d = NamedDomainOrCurrent(domain); + if (addr < 0) LogCallback($"Warning: Attempted reads on addresses {addr}..-1 outside range of domain {d.Name} in {nameof(ReadU16Range)}()"); + var lastReqAddr = addr + count * sizeof(ushort) - 1; + var indexAfterLast = Math.Min(Math.Max(-1L, lastReqAddr), d.Size - 1L) + 1L; + var iSrc = Math.Min(Math.Max(0L, addr), d.Size); + var iDst = (iSrc - addr) / sizeof(ushort); + var values = new ushort[(indexAfterLast - iSrc) / sizeof(ushort)]; + if (iSrc < indexAfterLast) using (d.EnterExit()) d.BulkPeekUshort(iSrc.RangeToExclusive(indexAfterLast), _isBigEndian, values); + if (lastReqAddr >= d.Size) LogCallback($"Warning: Attempted reads on addresses {d.Size}..{lastReqAddr} outside range of domain {d.Name} in {nameof(ReadU16Range)}()"); + if (values.Length == count) return values; + var newValues = new ushort[count]; + if (values.Length is not 0) Array.Copy(sourceArray: values, sourceIndex: 0, destinationArray: newValues, destinationIndex: iDst, length: values.Length); + return newValues; + } + + public void WriteU16Range(long addr, Span memoryblock, string domain = null) + { + var d = NamedDomainOrCurrent(domain); + if (!d.Writable) + { + LogCallback($"Error: the domain {d.Name} is not writable"); + return; + } + if (addr < 0) LogCallback($"Warning: Attempted writes on addresses {addr}..-1 outside range of domain {d.Name} in {nameof(WriteU16Range)}()"); + var iDst = Math.Min(Math.Max(0L, addr), d.Size); + var iSrc = checked((int) (iDst - addr)) / sizeof(ushort); + var lastReqAddrExclusive = addr + memoryblock.Length * sizeof(ushort); + var lastAddressExclusive = Math.Min(Math.Max(0, lastReqAddrExclusive), d.Size); + var length = lastAddressExclusive - iDst; + if (length > 0) using (d.EnterExit()) d.BulkPokeUshort(iDst, _isBigEndian, memoryblock.Slice(start: iSrc, length: (int)(length / sizeof(ushort)))); + if (lastReqAddrExclusive >= d.Size) LogCallback($"Warning: Attempted writes on addresses {d.Size}..{lastReqAddrExclusive} outside range of domain {d.Name} in {nameof(WriteU16Range)}()"); + } + + public IReadOnlyList ReadU32Range(long addr, int count, string domain = null) + { + var d = NamedDomainOrCurrent(domain); + if (addr < 0) LogCallback($"Warning: Attempted reads on addresses {addr}..-1 outside range of domain {d.Name} in {nameof(ReadU32Range)}()"); + var lastReqAddr = addr + count * sizeof(uint) - 1; + var indexAfterLast = Math.Min(Math.Max(-1L, lastReqAddr), d.Size - 1L) + 1L; + var iSrc = Math.Min(Math.Max(0L, addr), d.Size); + var iDst = (iSrc - addr) / sizeof(uint); + var values = new uint[(indexAfterLast - iSrc) / sizeof(uint)]; + if (iSrc < indexAfterLast) using (d.EnterExit()) d.BulkPeekUint(iSrc.RangeToExclusive(indexAfterLast), _isBigEndian, values); + if (lastReqAddr >= d.Size) LogCallback($"Warning: Attempted reads on addresses {d.Size}..{lastReqAddr} outside range of domain {d.Name} in {nameof(ReadU32Range)}()"); + if (values.Length == count) return values; + var newValues = new uint[count]; + if (values.Length is not 0) Array.Copy(sourceArray: values, sourceIndex: 0, destinationArray: newValues, destinationIndex: iDst, length: values.Length); + return newValues; + } + + public void WriteU32Range(long addr, Span memoryblock, string domain = null) + { + var d = NamedDomainOrCurrent(domain); + if (!d.Writable) { - while (iDst < indexAfterLast) d.PokeByte(iDst++, memoryblock[iSrc++]); + LogCallback($"Error: the domain {d.Name} is not writable"); + return; } - if (lastReqAddr >= d.Size) LogCallback($"Warning: Attempted writes on addresses {d.Size}..{lastReqAddr} outside range of domain {d.Name} in {nameof(WriteByteRange)}()"); + if (addr < 0) LogCallback($"Warning: Attempted writes on addresses {addr}..-1 outside range of domain {d.Name} in {nameof(WriteU32Range)}()"); + var iDst = Math.Min(Math.Max(0L, addr), d.Size); + var iSrc = checked((int) (iDst - addr)) / sizeof(uint); + var lastReqAddrExclusive = addr + memoryblock.Length * sizeof(uint); + var lastAddressExclusive = Math.Min(Math.Max(0, lastReqAddrExclusive), d.Size); + var length = lastAddressExclusive - iDst; + if (length > 0) using (d.EnterExit()) d.BulkPokeUint(iDst, _isBigEndian, memoryblock.Slice(start: iSrc, length: (int)(length / sizeof(uint)))); + if (lastReqAddrExclusive >= d.Size) LogCallback($"Warning: Attempted writes on addresses {d.Size}..{lastReqAddrExclusive} outside range of domain {d.Name} in {nameof(WriteU32Range)}()"); } public float ReadFloat(long addr, string domain = null) diff --git a/src/BizHawk.Client.Common/Api/Interfaces/IMemoryApi.cs b/src/BizHawk.Client.Common/Api/Interfaces/IMemoryApi.cs index ae644937a04..65e3df7e47c 100644 --- a/src/BizHawk.Client.Common/Api/Interfaces/IMemoryApi.cs +++ b/src/BizHawk.Client.Common/Api/Interfaces/IMemoryApi.cs @@ -30,9 +30,14 @@ public interface IMemoryApi : IExternalApi uint ReadU32(long addr, string domain = null); void WriteByte(long addr, uint value, string domain = null); - void WriteByteRange(long addr, IReadOnlyList memoryblock, string domain = null); + void WriteByteRange(long addr, Span memoryblock, string domain = null); void WriteFloat(long addr, float value, string domain = null); + IReadOnlyList ReadU16Range(long addr, int count, string domain = null); + IReadOnlyList ReadU32Range(long addr, int count, string domain = null); + void WriteU16Range(long addr, Span memoryblock, string domain = null); + void WriteU32Range(long addr, Span memoryblock, string domain = null); + void WriteS8(long addr, int value, string domain = null); void WriteS16(long addr, int value, string domain = null); void WriteS24(long addr, int value, string domain = null); diff --git a/src/BizHawk.Client.Common/lua/CommonLibs/MemoryLuaLibrary.cs b/src/BizHawk.Client.Common/lua/CommonLibs/MemoryLuaLibrary.cs index 3f09befa5d4..8e054f3edbb 100644 --- a/src/BizHawk.Client.Common/lua/CommonLibs/MemoryLuaLibrary.cs +++ b/src/BizHawk.Client.Common/lua/CommonLibs/MemoryLuaLibrary.cs @@ -120,7 +120,7 @@ public void WriteByteRange([LuaZeroIndexed] LuaTable memoryblock, string domain [LuaMethodExample("memory.write_bytes_as_array(0x100, { 0xAB, 0x12, 0xCD, 0x34 });")] [LuaMethod("write_bytes_as_array", "Writes sequential bytes starting at addr.")] public void WriteBytesAsArray(long addr, LuaTable bytes, string domain = null) - => APIs.Memory.WriteByteRange(addr, _th.EnumerateValues(bytes).Select(l => (byte) l).ToList(), domain); + => APIs.Memory.WriteByteRange(addr, _th.EnumerateValues(bytes).Select(l => (byte) l).ToArray(), domain); [LuaMethodExample("memory.write_bytes_as_dict({ [0x100] = 0xAB, [0x104] = 0xCD, [0x106] = 0x12, [0x107] = 0x34, [0x108] = 0xEF });")] [LuaMethod("write_bytes_as_dict", "Writes bytes at arbitrary addresses (the keys of the given table are the addresses, relative to the start of the domain).")] @@ -132,6 +132,70 @@ public void WriteBytesAsDict([LuaZeroIndexed] LuaTable addrMap, string domain = } } + [LuaMethodExample("local values = memory.read_u16_le_as_array(0x100, 30, \"WRAM\");")] + [LuaMethod("read_u16_le_as_array", "Reads count 16-bit values starting at addr into an array-like table (1-indexed).")] + public LuaTable ReadUshortsAsArray(long addr, int count, string domain = null) + { + APIs.Memory.SetBigEndian(false); + return _th.ListToTable(APIs.Memory.ReadU16Range(addr, count, domain)); + } + + [LuaMethodExample("memory.write_u16_le_as_array(0x100, { 0xABCD, 0x1234 });")] + [LuaMethod("write_u16_le_as_array", "Writes sequential 16-byte values starting at addr.")] + public void WriteUshortsAsArray(long addr, LuaTable values, string domain = null) + { + APIs.Memory.SetBigEndian(false); + APIs.Memory.WriteU16Range(addr, _th.EnumerateValues(values).Select(l => (ushort) l).ToArray(), domain); + } + + [LuaMethodExample("local values = memory.read_u16_be_as_array(0x100, 30, \"WRAM\");")] + [LuaMethod("read_u16_be_as_array", "Reads count 16-bit values starting at addr into an array-like table (1-indexed).")] + public LuaTable ReadUshortsBigAsArray(long addr, int count, string domain = null) + { + APIs.Memory.SetBigEndian(true); + return _th.ListToTable(APIs.Memory.ReadU16Range(addr, count, domain)); + } + + [LuaMethodExample("memory.write_u16_be_as_array(0x100, { 0xABCD, 0x1234 });")] + [LuaMethod("write_u16_be_as_array", "Writes sequential 16-byte values starting at addr.")] + public void WriteUshortsAsBigArray(long addr, LuaTable values, string domain = null) + { + APIs.Memory.SetBigEndian(true); + APIs.Memory.WriteU16Range(addr, _th.EnumerateValues(values).Select(l => (ushort) l).ToArray(), domain); + } + + [LuaMethodExample("local values = memory.read_u32_le_as_array(0x100, 30, \"WRAM\");")] + [LuaMethod("read_u32_le_as_array", "Reads count 32-bit values starting at addr into an array-like table (1-indexed).")] + public LuaTable ReadUintsAsArray(long addr, int count, string domain = null) + { + APIs.Memory.SetBigEndian(false); + return _th.ListToTable(APIs.Memory.ReadU32Range(addr, count, domain)); + } + + [LuaMethodExample("memory.write_u32_le_as_array(0x100, { 0xABCD, 0x1234 });")] + [LuaMethod("write_u32_le_as_array", "Writes sequential 32-byte values starting at addr.")] + public void WriteUintsAsArray(long addr, LuaTable values, string domain = null) + { + APIs.Memory.SetBigEndian(false); + APIs.Memory.WriteU32Range(addr, _th.EnumerateValues(values).Select(l => (uint) l).ToArray(), domain); + } + + [LuaMethodExample("local values = memory.read_u32_be_as_array(0x100, 30, \"WRAM\");")] + [LuaMethod("read_u32_be_as_array", "Reads count 32-bit values starting at addr into an array-like table (1-indexed).")] + public LuaTable ReadUintsBigAsArray(long addr, int count, string domain = null) + { + APIs.Memory.SetBigEndian(true); + return _th.ListToTable(APIs.Memory.ReadU32Range(addr, count, domain)); + } + + [LuaMethodExample("memory.write_u32_be_as_array(0x100, { 0xABCD, 0x1234 });")] + [LuaMethod("write_u32_be_as_array", "Writes sequential 32-byte values starting at addr.")] + public void WriteUintsBigAsArray(long addr, LuaTable values, string domain = null) + { + APIs.Memory.SetBigEndian(true); + APIs.Memory.WriteU32Range(addr, _th.EnumerateValues(values).Select(l => (uint) l).ToArray(), domain); + } + [LuaMethodExample(""" memory.write_bytes_as_binary_string(0x100, string.pack(" APIs.Memory.WriteByteRange(addr, _th.EnumerateValues(bytes).Select(l => (byte) l).ToList(), MainMemName); + => APIs.Memory.WriteByteRange(addr, _th.EnumerateValues(bytes).Select(l => (byte) l).ToArray(), MainMemName); [LuaMethodExample("mainmemory.write_bytes_as_dict({ [0x100] = 0xAB, [0x104] = 0xCD, [0x106] = 0x12, [0x107] = 0x34, [0x108] = 0xEF });")] [LuaMethod("write_bytes_as_dict", "Writes bytes at arbitrary addresses (the keys of the given table are the addresses, relative to the start of the main memory).")] diff --git a/src/BizHawk.Emulation.Common/Base Implementations/MemoryDomain.cs b/src/BizHawk.Emulation.Common/Base Implementations/MemoryDomain.cs index 7416c8d9e6c..fa197325f9d 100644 --- a/src/BizHawk.Emulation.Common/Base Implementations/MemoryDomain.cs +++ b/src/BizHawk.Emulation.Common/Base Implementations/MemoryDomain.cs @@ -22,8 +22,6 @@ public enum Endian private const string ERR_MSG_BUFFER_WRONG_SIZE = "Invalid length of values array"; - private const string ERR_MSG_UNALIGNED = "The API contract doesn't define what to do for unaligned reads and writes!"; - public string Name { get; protected set; } public long Size { get; protected set; } @@ -93,7 +91,7 @@ public virtual void BulkPeekByte(Range addresses, byte[] values) { if (addresses is null) throw new ArgumentNullException(paramName: nameof(addresses)); if (values is null) throw new ArgumentNullException(paramName: nameof(values)); - if ((long) addresses.Count() != values.Length) throw new InvalidOperationException(ERR_MSG_BUFFER_WRONG_SIZE); + if ((long) addresses.Count() > values.Length) throw new InvalidOperationException(ERR_MSG_BUFFER_WRONG_SIZE); using (this.EnterExit()) { @@ -104,6 +102,18 @@ public virtual void BulkPeekByte(Range addresses, byte[] values) } } + public virtual void BulkPokeByte(long startAddress, Span values) + { + using (this.EnterExit()) + { + long address = startAddress; + for (var i = 0; i < values.Length; i++, address++) + { + PokeByte(address, values[i]); + } + } + } + public virtual void BulkPeekUshort(Range addresses, bool bigEndian, ushort[] values) { if (addresses is null) throw new ArgumentNullException(paramName: nameof(addresses)); @@ -111,8 +121,7 @@ public virtual void BulkPeekUshort(Range addresses, bool bigEndian, ushort var start = addresses.Start; var end = addresses.EndInclusive + 1; - if ((start & 0b1) is not 0 || (end & 0b1) is not 0) throw new InvalidOperationException(ERR_MSG_UNALIGNED); - if (values.LongLength * sizeof(ushort) != end - start) throw new InvalidOperationException(ERR_MSG_BUFFER_WRONG_SIZE); // a longer array could be valid, but nothing needs that so don't support it for now + if (values.LongLength * sizeof(ushort) < end - start) throw new InvalidOperationException(ERR_MSG_BUFFER_WRONG_SIZE); using (this.EnterExit()) { @@ -123,6 +132,18 @@ public virtual void BulkPeekUshort(Range addresses, bool bigEndian, ushort } } + public virtual void BulkPokeUshort(long startAddress, bool bigEndian, Span values) + { + using (this.EnterExit()) + { + long address = startAddress; + for (var i = 0; i < values.Length; i++, address += sizeof(ushort)) + { + PokeUshort(address, values[i], bigEndian); + } + } + } + public virtual void BulkPeekUint(Range addresses, bool bigEndian, uint[] values) { if (addresses is null) throw new ArgumentNullException(paramName: nameof(addresses)); @@ -130,8 +151,7 @@ public virtual void BulkPeekUint(Range addresses, bool bigEndian, uint[] v var start = addresses.Start; var end = addresses.EndInclusive + 1; - if ((start & 0b11) is not 0 || (end & 0b11) is not 0) throw new InvalidOperationException(ERR_MSG_UNALIGNED); - if (values.LongLength * sizeof(uint) != end - start) throw new InvalidOperationException(ERR_MSG_BUFFER_WRONG_SIZE); // `!=` not `<`, per `BulkPeekUshort` + if (values.LongLength * sizeof(uint) < end - start) throw new InvalidOperationException(ERR_MSG_BUFFER_WRONG_SIZE); using (this.EnterExit()) { @@ -142,6 +162,18 @@ public virtual void BulkPeekUint(Range addresses, bool bigEndian, uint[] v } } + public virtual void BulkPokeUint(long startAddress, bool bigEndian, Span values) + { + using (this.EnterExit()) + { + long address = startAddress; + for (var i = 0; i < values.Length; i++, address += sizeof(uint)) + { + PokeUint(address, values[i], bigEndian); + } + } + } + public virtual void SendCheatToCore(int addr, byte value, int compare, int compare_type) { } /// diff --git a/src/BizHawk.Emulation.Cores/Computers/Doom/DSDA.IMemoryDomains.cs b/src/BizHawk.Emulation.Cores/Computers/Doom/DSDA.IMemoryDomains.cs index 2f0ee886ec6..7734e329e56 100644 --- a/src/BizHawk.Emulation.Cores/Computers/Doom/DSDA.IMemoryDomains.cs +++ b/src/BizHawk.Emulation.Cores/Computers/Doom/DSDA.IMemoryDomains.cs @@ -232,12 +232,8 @@ public override void BulkPeekUshort(Range addresses, bool bigEndian, ushor var start = addresses.Start; var end = addresses.EndInclusive + 1; - if ((start & 1) != 0 || (end & 1) != 0) - throw new InvalidOperationException("The API contract doesn't define what to do for unaligned reads and writes!"); - - if (values.LongLength * 2 != end - start) + if (values.LongLength * sizeof(ushort) < end - start) { - // a longer array could be valid, but nothing needs that so don't support it for now throw new InvalidOperationException("Invalid length of values array"); } @@ -262,12 +258,8 @@ public override void BulkPeekUint(Range addresses, bool bigEndian, uint[] var start = addresses.Start; var end = addresses.EndInclusive + 1; - if ((start & 3) != 0 || (end & 3) != 0) - throw new InvalidOperationException("The API contract doesn't define what to do for unaligned reads and writes!"); - - if (values.LongLength * 4 != end - start) + if (values.LongLength * sizeof(uint) < end - start) { - // a longer array could be valid, but nothing needs that so don't support it for now throw new InvalidOperationException("Invalid length of values array"); } diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/3DS/Encore.IMemoryDomains.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/3DS/Encore.IMemoryDomains.cs index 4c744da4ec5..4f21da38e5f 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/3DS/Encore.IMemoryDomains.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/3DS/Encore.IMemoryDomains.cs @@ -222,12 +222,8 @@ public override void BulkPeekUshort(Range addresses, bool bigEndian, ushor var start = addresses.Start; var end = addresses.EndInclusive + 1; - if ((start & 1) != 0 || (end & 1) != 0) - throw new InvalidOperationException("The API contract doesn't define what to do for unaligned reads and writes!"); - - if (values.LongLength * 2 != end - start) + if (values.LongLength * sizeof(ushort) < end - start) { - // a longer array could be valid, but nothing needs that so don't support it for now throw new InvalidOperationException("Invalid length of values array"); } @@ -250,12 +246,8 @@ public override void BulkPeekUint(Range addresses, bool bigEndian, uint[] var start = addresses.Start; var end = addresses.EndInclusive + 1; - if ((start & 3) != 0 || (end & 3) != 0) - throw new InvalidOperationException("The API contract doesn't define what to do for unaligned reads and writes!"); - - if (values.LongLength * 4 != end - start) + if (values.LongLength * sizeof(uint) < end - start) { - // a longer array could be valid, but nothing needs that so don't support it for now throw new InvalidOperationException("Invalid length of values array"); } diff --git a/src/BizHawk.Emulation.Cores/Waterbox/WaterboxMemoryDomain.cs b/src/BizHawk.Emulation.Cores/Waterbox/WaterboxMemoryDomain.cs index 751d7125c11..9105e01e58c 100644 --- a/src/BizHawk.Emulation.Cores/Waterbox/WaterboxMemoryDomain.cs +++ b/src/BizHawk.Emulation.Cores/Waterbox/WaterboxMemoryDomain.cs @@ -338,7 +338,7 @@ public override void BulkPeekByte(Range addresses, byte[] values) _read8.Access((IntPtr)p, addresses.Start, (long)addresses.Count()); } - public void BulkPokeByte(long addr, Span values) + public override void BulkPokeByte(long addr, Span values) { if ((ulong)addr + (ulong)values.Length > (ulong)Size || addr < 0) throw new ArgumentOutOfRangeException(nameof(addr), message: AddressRangeError); diff --git a/src/BizHawk.Tests.Client.Common/Api/MemoryApiTests.cs b/src/BizHawk.Tests.Client.Common/Api/MemoryApiTests.cs index c26466960bb..f242ca0d6c5 100644 --- a/src/BizHawk.Tests.Client.Common/Api/MemoryApiTests.cs +++ b/src/BizHawk.Tests.Client.Common/Api/MemoryApiTests.cs @@ -19,7 +19,7 @@ private IMemoryApi CreateDummyApi(byte[] memDomainContents) }; [TestMethod] - public void TestBulkPeek() + public void TestBulkPeekByte() { var memApi = CreateDummyApi(new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF }); CollectionAssert.That.AreEqual( @@ -57,7 +57,7 @@ public void TestBulkPeek() } [TestMethod] - public void TestBulkPoke() + public void TestBulkPokeByte() { void TestCase(IReadOnlyList expected, Action action, string message) { @@ -99,6 +99,198 @@ void TestCase(IReadOnlyList expected, Action action, string me "fully above upper boundary"); } + [TestMethod] + public void TestBulkPeekUshort() + { + ushort[] memContents = new ushort[] { 0x0123, 0x4567, 0x89AB, 0xCDEF }; + byte[] memBytes = new byte[8]; + unsafe + { + fixed (byte* dst = memBytes) + { + ushort* dstUshort = (ushort*)dst; + for (int i = 0; i < memContents.Length; i++) dstUshort[i] = memContents[i]; + } + } + var memApi = CreateDummyApi(memBytes); + CollectionAssert.That.AreEqual( + new ushort[] { default, default }, + memApi.ReadU16Range(addr: -6, count: 2), + "fully below lower boundary"); + CollectionAssert.That.AreEqual( + new ushort[] { default, 0x0123 }, + memApi.ReadU16Range(addr: -2, count: 2), + "crosses lower boundary"); + CollectionAssert.That.AreEqual( + new ushort[] { default, 0x0123, 0x4567, 0x89AB, 0xCDEF, default }, + memApi.ReadU16Range(addr: -2, count: 6), + "crosses both boundaries"); + CollectionAssert.That.AreEqual( + new ushort[] { 0x0123, 0x4567, 0x89AB, 0xCDEF }, + memApi.ReadU16Range(addr: 0, count: 4), + "whole domain"); + CollectionAssert.That.AreEqual( + new ushort[] { 0x4567, 0x89AB, 0xCDEF }, + memApi.ReadU16Range(addr: 2, count: 3), + "strict contains"); + CollectionAssert.That.AreEqual( + Array.Empty(), + memApi.ReadU16Range(addr: 2, count: 0), + "empty"); + CollectionAssert.That.AreEqual( + new ushort[] { 0xCDEF, default }, + memApi.ReadU16Range(addr: 6, count: 2), + "crosses upper boundary"); + CollectionAssert.That.AreEqual( + new ushort[] { default, default }, + memApi.ReadU16Range(addr: 8, count: 2), + "fully above upper boundary"); + } + + [TestMethod] + public void TestBulkPokeUshort() + { + unsafe void TestCase(IReadOnlyList expected, Action action, string message) + { + var memDomainContents = new byte[8]; + action(CreateDummyApi(memDomainContents)); + ushort[] memUshorts = new ushort[4]; + fixed (byte* memBytes = memDomainContents) + { + for (int i = 0; i < memUshorts.Length; i++) memUshorts[i] = *(ushort*)(memBytes + i * sizeof(ushort)); + } + CollectionAssert.That.AreEqual(expected, memUshorts, message); + } + TestCase( + new ushort[4], + memApi => memApi.WriteU16Range(-4, new ushort[] { 0x0123, 0x4567 }), + "fully below lower boundary"); + TestCase( + new ushort[] { 0x4567, default, default, default }, + memApi => memApi.WriteU16Range(-2, new ushort[] { 0x0123, 0x4567 }), + "crosses lower boundary"); + TestCase( + new ushort[] { 0x4567, 0x89AB, 0xCDEF, 0xFEDC }, + memApi => memApi.WriteU16Range(-2, new ushort[] { 0x0123, 0x4567, 0x89AB, 0xCDEF, 0xFEDC, 0xBA98 }), + "crosses both boundaries"); + TestCase( + new ushort[] { 0x0123, 0x4567, 0x89AB, 0xCDEF }, + memApi => memApi.WriteU16Range(0, new ushort[] { 0x0123, 0x4567, 0x89AB, 0xCDEF }), + "whole domain"); + TestCase( + new ushort[] { default, 0x0123, 0x4567, 0x89AB }, + memApi => memApi.WriteU16Range(2, new ushort[] { 0x0123, 0x4567, 0x89AB }), + "strict contains"); + TestCase( + new ushort[4], + memApi => memApi.WriteU16Range(2, Array.Empty()), + "empty"); + TestCase( + new ushort[] { default, default, default, 0x0123 }, + memApi => memApi.WriteU16Range(6, new ushort[] { 0x0123, 0x4567 }), + "crosses upper boundary"); + TestCase( + new ushort[4], + memApi => memApi.WriteU16Range(8, new ushort[] { 0x0123, 0x4567 }), + "fully above upper boundary"); + } + + [TestMethod] + public void TestBulkPeekUint() + { + uint[] memContents = new uint[] { 0x01234567, 0x89ABCDEF }; + byte[] memBytes = new byte[8]; + unsafe + { + fixed (byte* dst = memBytes) + { + uint* dstUint = (uint*)dst; + for (int i = 0; i < memContents.Length; i++) dstUint[i] = memContents[i]; + } + } + var memApi = CreateDummyApi(memBytes); + CollectionAssert.That.AreEqual( + new uint[] { default, default }, + memApi.ReadU32Range(addr: -10, count: 2), + "fully below lower boundary"); + CollectionAssert.That.AreEqual( + new uint[] { default, 0x01234567 }, + memApi.ReadU32Range(addr: -4, count: 2), + "crosses lower boundary"); + CollectionAssert.That.AreEqual( + new uint[] { default, 0x01234567, 0x89ABCDEF, default }, + memApi.ReadU32Range(addr: -4, count: 4), + "crosses both boundaries"); + CollectionAssert.That.AreEqual( + new uint[] { 0x01234567, 0x89ABCDEF }, + memApi.ReadU32Range(addr: 0, count: 2), + "whole domain"); + CollectionAssert.That.AreEqual( + new uint[] { 0x89ABCDEF }, + memApi.ReadU32Range(addr: 4, count: 1), + "strict contains"); + CollectionAssert.That.AreEqual( + Array.Empty(), + memApi.ReadU32Range(addr: 0, count: 0), + "empty"); + CollectionAssert.That.AreEqual( + new uint[] { 0x89ABCDEF, default }, + memApi.ReadU32Range(addr: 4, count: 2), + "crosses upper boundary"); + CollectionAssert.That.AreEqual( + new uint[] { default, default }, + memApi.ReadU32Range(addr: 8, count: 2), + "fully above upper boundary"); + } + + [TestMethod] + public void TestBulkPokeUint() + { + unsafe void TestCase(IReadOnlyList expected, Action action, string message) + { + var memDomainContents = new byte[8]; + action(CreateDummyApi(memDomainContents)); + uint[] memUints = new uint[2]; + fixed (byte* memBytes = memDomainContents) + { + for (int i = 0; i < memUints.Length; i++) memUints[i] = *(uint*)(memBytes + i * sizeof(uint)); + } + CollectionAssert.That.AreEqual(expected, memUints, message); + } + TestCase( + new uint[2], + memApi => memApi.WriteU32Range(-8, new uint[] { 0x01234567, 0x89ABCDEF }), + "fully below lower boundary"); + TestCase( + new uint[] { 0x89ABCDEF, default }, + memApi => memApi.WriteU32Range(-4, new uint[] { 0x01234567, 0x89ABCDEF }), + "crosses lower boundary"); + TestCase( + new uint[] { 0x89ABCDEF, 0xFEDCBA98 }, + memApi => memApi.WriteU32Range(-4, new uint[] { 0x01234567, 0x89ABCDEF, 0xFEDCBA98, 0x76543210 }), + "crosses both boundaries"); + TestCase( + new uint[] { 0x01234567, 0x89ABCDEF }, + memApi => memApi.WriteU32Range(0, new uint[] { 0x01234567, 0x89ABCDEF }), + "whole domain"); + TestCase( + new uint[] { default, 0x01234567 }, + memApi => memApi.WriteU32Range(4, new uint[] { 0x01234567 }), + "strict contains"); + TestCase( + new uint[2], + memApi => memApi.WriteU32Range(0, Array.Empty()), + "empty"); + TestCase( + new uint[] { default, 0x01234567 }, + memApi => memApi.WriteU32Range(4, new uint[] { 0x01234567, 0x89ABCDEF }), + "crosses upper boundary"); + TestCase( + new uint[2], + memApi => memApi.WriteU32Range(8, new uint[] { 0x01234567, 0x89ABCDEF }), + "fully above upper boundary"); + } + [TestMethod] public void TestHash() {