Fix get code regions to adhere to memory bus override regions

This commit is contained in:
KallDrexx
2026-01-12 21:49:46 -05:00
parent ccb2aaa7ed
commit c5ece68895
4 changed files with 348 additions and 5 deletions
@@ -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)
{
+125 -4
View File
@@ -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
}
}