mirror of
https://github.com/ApfelTeeSaft/DotNesJit.git
synced 2026-08-26 19:23:35 +00:00
Fix get code regions to adhere to memory bus override regions
This commit is contained in:
@@ -34,6 +34,10 @@ public class ProgrammableLogicArray : IMemoryDevice
|
||||
_memoryConfig.CpuMemoryBus.Attach(_d000ToDfffDevice, 0xd000, true);
|
||||
_memoryConfig.CpuMemoryBus.Attach(_e000ToFfffDevice, 0xe000, true);
|
||||
|
||||
_a000ToBfffDevice.SetRoutableDevice(memoryConfig.FullRam, RoutableMemoryDevice.RoutableDirection.ReadAndWrite);
|
||||
_d000ToDfffDevice.SetRoutableDevice(memoryConfig.FullRam, RoutableMemoryDevice.RoutableDirection.ReadAndWrite);
|
||||
_e000ToFfffDevice.SetRoutableDevice(memoryConfig.FullRam, RoutableMemoryDevice.RoutableDirection.ReadAndWrite);
|
||||
|
||||
UpdateDevices();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public class RoutableMemoryDevice : IMemoryDevice
|
||||
private IMemoryDevice? _routableDeviceForWrite;
|
||||
|
||||
public uint Size { get; }
|
||||
public ReadOnlyMemory<byte>? RawBlockFromZero => null;
|
||||
public ReadOnlyMemory<byte>? RawBlockFromZero => _routableDeviceForRead!.RawBlockFromZero;
|
||||
|
||||
public RoutableMemoryDevice(uint size)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace Dotnet6502.Common.Hardware;
|
||||
public class MemoryBus
|
||||
{
|
||||
private record AttachedDevice(ushort BaseAddress, IMemoryDevice Device);
|
||||
private record VisibleRange(ushort DeviceIndex, int StartAddress, int EndAddress);
|
||||
|
||||
private readonly List<AttachedDevice> _devices = [];
|
||||
|
||||
@@ -17,6 +18,11 @@ public class MemoryBus
|
||||
/// </summary>
|
||||
private readonly ushort[] _deviceIndexMap;
|
||||
|
||||
/// <summary>
|
||||
/// Cached list of visible memory ranges (which device is visible at which addresses).
|
||||
/// </summary>
|
||||
private List<VisibleRange> _cachedVisibleRanges = [];
|
||||
|
||||
public MemoryBus(int memorySize)
|
||||
{
|
||||
_deviceIndexMap = new ushort[memorySize];
|
||||
@@ -78,6 +84,9 @@ public class MemoryBus
|
||||
{
|
||||
_deviceIndexMap[baseAddress + x] = (ushort)deviceIndex;
|
||||
}
|
||||
|
||||
// Rebuild visible ranges cache to reflect the new memory mapping
|
||||
RebuildVisibleRangesCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -111,11 +120,123 @@ public class MemoryBus
|
||||
return attachment.Device.Read((ushort)offset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the cached visible ranges by scanning the device index map to find
|
||||
/// which device is visible at which addresses, accounting for overrides and fragmentation.
|
||||
/// </summary>
|
||||
private void RebuildVisibleRangesCache()
|
||||
{
|
||||
var visibleRanges = new List<VisibleRange>();
|
||||
var currentStart = -1;
|
||||
ushort currentDeviceIndex = 0;
|
||||
|
||||
// Scan _deviceIndexMap to find continuous visible ranges
|
||||
for (var addr = 0; addr < _deviceIndexMap.Length; addr++)
|
||||
{
|
||||
var deviceIndex = _deviceIndexMap[addr];
|
||||
|
||||
if (deviceIndex != currentDeviceIndex)
|
||||
{
|
||||
// Range boundary - save previous range if it was mapped
|
||||
if (currentStart >= 0 && currentDeviceIndex > 0)
|
||||
{
|
||||
SplitAndAddRanges(visibleRanges, currentDeviceIndex, currentStart, addr);
|
||||
}
|
||||
|
||||
// Start new range (or mark as unmapped if deviceIndex is 0)
|
||||
currentStart = deviceIndex > 0 ? addr : -1;
|
||||
currentDeviceIndex = deviceIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last range if it extends to the end of memory
|
||||
if (currentStart >= 0 && currentDeviceIndex > 0)
|
||||
{
|
||||
SplitAndAddRanges(visibleRanges, currentDeviceIndex, currentStart, _deviceIndexMap.Length);
|
||||
}
|
||||
|
||||
_cachedVisibleRanges = visibleRanges;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a range into multiple ranges if it exceeds the device size (for mirrored devices).
|
||||
/// </summary>
|
||||
private void SplitAndAddRanges(List<VisibleRange> ranges, ushort deviceIndex, int startAddress, int endAddress)
|
||||
{
|
||||
var device = _devices[deviceIndex - 1].Device;
|
||||
var rangeLength = endAddress - startAddress;
|
||||
|
||||
// If the range is larger than the device size, split it into multiple ranges
|
||||
// This handles mirrored devices (same device attached at multiple addresses)
|
||||
if (rangeLength > device.Size)
|
||||
{
|
||||
var currentAddr = startAddress;
|
||||
while (currentAddr < endAddress)
|
||||
{
|
||||
var chunkEnd = (int)Math.Min(currentAddr + device.Size, endAddress);
|
||||
ranges.Add(new VisibleRange(
|
||||
DeviceIndex: deviceIndex,
|
||||
StartAddress: currentAddr,
|
||||
EndAddress: chunkEnd
|
||||
));
|
||||
currentAddr = chunkEnd;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal range (not mirrored or fragmented within device bounds)
|
||||
ranges.Add(new VisibleRange(
|
||||
DeviceIndex: deviceIndex,
|
||||
StartAddress: startAddress,
|
||||
EndAddress: endAddress
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<CodeRegion> GetAllCodeRegions()
|
||||
{
|
||||
return _devices.Select(x => new { x.BaseAddress, x.Device.RawBlockFromZero })
|
||||
.Where(x => x.RawBlockFromZero != null)
|
||||
.Select(x => new CodeRegion(x.BaseAddress, x.RawBlockFromZero!.Value))
|
||||
.ToArray();
|
||||
var regions = new List<CodeRegion>();
|
||||
|
||||
// Generate CodeRegions from cached visible ranges using current memory content
|
||||
foreach (var range in _cachedVisibleRanges)
|
||||
{
|
||||
var attachment = _devices[range.DeviceIndex - 1];
|
||||
var device = attachment.Device;
|
||||
var baseAddress = attachment.BaseAddress;
|
||||
|
||||
// Skip devices without raw memory blocks (like IoMemoryArea, RoutableMemoryDevice)
|
||||
if (device.RawBlockFromZero == null)
|
||||
continue;
|
||||
|
||||
var rawBlock = device.RawBlockFromZero.Value;
|
||||
|
||||
// Calculate the offset into the device's memory
|
||||
// Use modulo to handle mirrored devices (same device attached at multiple addresses)
|
||||
var offsetInDevice = (uint)(range.StartAddress - baseAddress);
|
||||
var deviceOffsetStart = offsetInDevice % device.Size;
|
||||
var length = range.EndAddress - range.StartAddress;
|
||||
|
||||
// Validate before slicing
|
||||
if (deviceOffsetStart + length > device.Size)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid slice calculation: offset={deviceOffsetStart}, length={length}, device.Size={device.Size}, " +
|
||||
$"range={range.StartAddress:X4}-{range.EndAddress:X4}, baseAddress={baseAddress:X4}");
|
||||
}
|
||||
|
||||
if (deviceOffsetStart + length > rawBlock.Length)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Slice would exceed rawBlock: offset={deviceOffsetStart}, length={length}, rawBlock.Length={rawBlock.Length}, " +
|
||||
$"device.Size={device.Size}, range={range.StartAddress:X4}-{range.EndAddress:X4}, baseAddress={baseAddress:X4}");
|
||||
}
|
||||
|
||||
// Slice the device's memory to get the current visible portion
|
||||
var slicedMemory = rawBlock.Slice((int)deviceOffsetStart, length);
|
||||
|
||||
regions.Add(new CodeRegion((ushort)range.StartAddress, slicedMemory));
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using Dotnet6502.Common.Hardware;
|
||||
using Shouldly;
|
||||
|
||||
namespace Dotnet6502.Tests.Common.Hardware;
|
||||
|
||||
public class MemoryBusTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetAllCodeRegions_Returns_Single_Device_Fully_Visible()
|
||||
{
|
||||
var bus = new MemoryBus(0x10000);
|
||||
var ram = new BasicRamMemoryDevice(0x1000);
|
||||
bus.Attach(ram, 0x0000);
|
||||
|
||||
var regions = bus.GetAllCodeRegions();
|
||||
|
||||
regions.Count.ShouldBe(1);
|
||||
regions[0].BaseAddress.ShouldBe((ushort)0x0000);
|
||||
regions[0].Bytes.Length.ShouldBe(0x1000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllCodeRegions_Excludes_Completely_Overridden_Device()
|
||||
{
|
||||
var bus = new MemoryBus(0x10000);
|
||||
var ram = new BasicRamMemoryDevice(0x1000);
|
||||
var rom = new BasicRamMemoryDevice(0x1000);
|
||||
|
||||
bus.Attach(ram, 0x0000);
|
||||
bus.Attach(rom, 0x0000, allowsOverriding: true);
|
||||
|
||||
var regions = bus.GetAllCodeRegions();
|
||||
|
||||
// should only see ROM, not RAM
|
||||
regions.Count.ShouldBe(1);
|
||||
regions[0].BaseAddress.ShouldBe((ushort)0x0000);
|
||||
|
||||
// Verify it's the ROM by checking that the spans have the same content
|
||||
regions[0].Bytes.Span.SequenceEqual(rom.RawBlockFromZero!.Value.Span).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllCodeRegions_Handles_Partial_Override_Fragmentation()
|
||||
{
|
||||
var bus = new MemoryBus(0x10000);
|
||||
var ram = new BasicRamMemoryDevice(0x4000); // 0x0000-0x3FFF
|
||||
var rom = new BasicRamMemoryDevice(0x1000); // Will override 0x2000-0x2FFF
|
||||
|
||||
bus.Attach(ram, 0x0000);
|
||||
bus.Attach(rom, 0x2000, allowsOverriding: true);
|
||||
|
||||
var regions = bus.GetAllCodeRegions();
|
||||
|
||||
// should see RAM fragmented around ROM
|
||||
regions.Count.ShouldBe(3);
|
||||
|
||||
// First RAM fragment: 0x0000-0x1FFF
|
||||
regions[0].BaseAddress.ShouldBe((ushort)0x0000);
|
||||
regions[0].Bytes.Length.ShouldBe(0x2000);
|
||||
|
||||
// ROM: 0x2000-0x2FFF
|
||||
regions[1].BaseAddress.ShouldBe((ushort)0x2000);
|
||||
regions[1].Bytes.Length.ShouldBe(0x1000);
|
||||
|
||||
// Second RAM fragment: 0x3000-0x3FFF
|
||||
regions[2].BaseAddress.ShouldBe((ushort)0x3000);
|
||||
regions[2].Bytes.Length.ShouldBe(0x1000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllCodeRegions_Handles_Mirrored_Devices()
|
||||
{
|
||||
// NES-style RAM mirroring
|
||||
var bus = new MemoryBus(0x10000);
|
||||
var ram = new BasicRamMemoryDevice(0x0800);
|
||||
|
||||
bus.Attach(ram, 0x0000);
|
||||
bus.Attach(ram, 0x0800);
|
||||
bus.Attach(ram, 0x1000);
|
||||
bus.Attach(ram, 0x1800);
|
||||
|
||||
var regions = bus.GetAllCodeRegions();
|
||||
|
||||
// should see 4 separate regions for each mirror
|
||||
regions.Count.ShouldBe(4);
|
||||
|
||||
regions[0].BaseAddress.ShouldBe((ushort)0x0000);
|
||||
regions[0].Bytes.Length.ShouldBe(0x0800);
|
||||
|
||||
regions[1].BaseAddress.ShouldBe((ushort)0x0800);
|
||||
regions[1].Bytes.Length.ShouldBe(0x0800);
|
||||
|
||||
regions[2].BaseAddress.ShouldBe((ushort)0x1000);
|
||||
regions[2].Bytes.Length.ShouldBe(0x0800);
|
||||
|
||||
regions[3].BaseAddress.ShouldBe((ushort)0x1800);
|
||||
regions[3].Bytes.Length.ShouldBe(0x0800);
|
||||
|
||||
// All should point to the same underlying memory
|
||||
regions[0].Bytes.Span.SequenceEqual(regions[1].Bytes.Span).ShouldBeTrue();
|
||||
regions[0].Bytes.Span.SequenceEqual(regions[2].Bytes.Span).ShouldBeTrue();
|
||||
regions[0].Bytes.Span.SequenceEqual(regions[3].Bytes.Span).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllCodeRegions_Excludes_Devices_Without_RawBlockFromZero()
|
||||
{
|
||||
var bus = new MemoryBus(0x10000);
|
||||
var ram = new BasicRamMemoryDevice(0x1000);
|
||||
var nullDevice = new NullMemoryDevice(0x1000);
|
||||
|
||||
bus.Attach(ram, 0x0000);
|
||||
bus.Attach(nullDevice, 0x1000);
|
||||
|
||||
var regions = bus.GetAllCodeRegions();
|
||||
|
||||
// should only see RAM, not NullMemoryDevice
|
||||
regions.Count.ShouldBe(1);
|
||||
regions[0].BaseAddress.ShouldBe((ushort)0x0000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllCodeRegions_Returns_Empty_List_For_Empty_Bus()
|
||||
{
|
||||
var bus = new MemoryBus(0x10000);
|
||||
var regions = bus.GetAllCodeRegions();
|
||||
regions.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllCodeRegions_Returns_Current_Memory_Content_After_Write()
|
||||
{
|
||||
var bus = new MemoryBus(0x10000);
|
||||
var ram = new BasicRamMemoryDevice(0x1000);
|
||||
bus.Attach(ram, 0x0000);
|
||||
|
||||
// write to memory
|
||||
bus.Write(0x0100, 0x42);
|
||||
var regions = bus.GetAllCodeRegions();
|
||||
|
||||
// should see the written value
|
||||
regions.Count.ShouldBe(1);
|
||||
regions[0].Bytes.Span[0x0100].ShouldBe((byte)0x42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllCodeRegions_Handles_Complex_C64_Style_Configuration()
|
||||
{
|
||||
// simulate C64 memory map with overlapping ROMs
|
||||
var bus = new MemoryBus(0x10000);
|
||||
var fullRam = new BasicRamMemoryDevice(0x10000);
|
||||
var charRom = new BasicRamMemoryDevice(0x1000);
|
||||
|
||||
// Full RAM covers everything
|
||||
bus.Attach(fullRam, 0x0000);
|
||||
|
||||
// Character ROM overlays at two locations
|
||||
bus.Attach(charRom, 0x1000, allowsOverriding: true);
|
||||
bus.Attach(charRom, 0x9000, allowsOverriding: true);
|
||||
|
||||
var regions = bus.GetAllCodeRegions();
|
||||
|
||||
// should see RAM fragmented with CharRom overlays
|
||||
regions.Count.ShouldBe(5);
|
||||
|
||||
// RAM: 0x0000-0x0FFF
|
||||
regions[0].BaseAddress.ShouldBe((ushort)0x0000);
|
||||
regions[0].Bytes.Length.ShouldBe(0x1000);
|
||||
|
||||
// CharRom: 0x1000-0x1FFF
|
||||
regions[1].BaseAddress.ShouldBe((ushort)0x1000);
|
||||
regions[1].Bytes.Length.ShouldBe(0x1000);
|
||||
|
||||
// RAM: 0x2000-0x8FFF
|
||||
regions[2].BaseAddress.ShouldBe((ushort)0x2000);
|
||||
regions[2].Bytes.Length.ShouldBe(0x7000);
|
||||
|
||||
// CharRom: 0x9000-0x9FFF
|
||||
regions[3].BaseAddress.ShouldBe((ushort)0x9000);
|
||||
regions[3].Bytes.Length.ShouldBe(0x1000);
|
||||
|
||||
// RAM: 0xA000-0xFFFF
|
||||
regions[4].BaseAddress.ShouldBe((ushort)0xA000);
|
||||
regions[4].Bytes.Length.ShouldBe(0x6000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllCodeRegions_Slices_Device_Memory_Correctly()
|
||||
{
|
||||
var bus = new MemoryBus(0x10000);
|
||||
var ram = new BasicRamMemoryDevice(0x1000);
|
||||
|
||||
// Write test pattern
|
||||
for (int i = 0; i < 0x1000; i++)
|
||||
{
|
||||
ram.Write((ushort)i, (byte)(i & 0xFF));
|
||||
}
|
||||
|
||||
var rom = new BasicRamMemoryDevice(0x0100);
|
||||
bus.Attach(ram, 0x0000);
|
||||
bus.Attach(rom, 0x0800, allowsOverriding: true); // Override middle section
|
||||
|
||||
var regions = bus.GetAllCodeRegions();
|
||||
regions.Count.ShouldBe(3);
|
||||
|
||||
// First RAM slice should start with correct offset
|
||||
regions[0].Bytes.Span[0].ShouldBe((byte)0x00);
|
||||
regions[0].Bytes.Span[0xFF].ShouldBe((byte)0xFF);
|
||||
|
||||
// ROM in middle
|
||||
regions[1].BaseAddress.ShouldBe((ushort)0x0800);
|
||||
regions[1].Bytes.Length.ShouldBe(0x0100);
|
||||
|
||||
// Second RAM slice should continue from correct offset
|
||||
regions[2].BaseAddress.ShouldBe((ushort)0x0900);
|
||||
regions[2].Bytes.Span[0].ShouldBe((byte)0x00); // Offset 0x900 in device
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user