Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CopperMod.Amiga.Tests/AmigaSpriteConformanceMatrixTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ private static IEnumerable<SpriteConformanceRow> MatrixRows
yield return SpriteConformanceRow.Executable("dma-list", "multiple control blocks reuse a channel");
yield return SpriteConformanceRow.Executable("dma-pointers", "SPRxPTL bit 0 ignored");
yield return SpriteConformanceRow.Executable("dma-timing", "extra-wide playfield fetches can consume late sprite DMA slots");
yield return SpriteConformanceRow.Executable("dma-timing", "sprite DMA latch is consumed after granted data fetch");
yield return SpriteConformanceRow.Executable("dma-timing", "live DMA sprite archive carries stationary command across missed capture frame");
yield return SpriteConformanceRow.Executable("dma-timing", "live DMA sprite archive does not carry across captured terminator");
yield return SpriteConformanceRow.Executable("dma-timing", "live DMA sprite archive does not carry stale command after control block rewrite");
Expand Down Expand Up @@ -1015,6 +1016,26 @@ public void TimedSpriteDmaUsesBusSlotsAndRecordsMissedSlots()
Assert.True(snapshot.LastMissedSpriteDmaSlots > 0);
}

[Fact]
public void SpriteDmaLatchIsConsumedAfterGrantedDataFetch()
{
var bus = CreateDisplayComponentBus();
EnableSpriteDma(bus, 0x8220);
SetColor(bus, SingleSpriteColorIndex(0, 1), 0x0F00);
WriteSpriteDmaBlock(bus, SpriteListBase, StandardX, StandardY, 1, 0x8000, 0x0000);
SetSpritePointer(bus, sprite: 0, SpriteListBase);
var frame = new uint[AmigaConstants.PalLowResWidth * AmigaConstants.PalLowResHeight];

bus.Display.RenderFrame(frame, 0, FrameCycles());

var latch = GetPrivateField<object>(bus.Display, "_spriteDmaReadLatch");
var hasValue = (bool)latch.GetType().GetProperty("HasValue")!.GetValue(latch)!;
var snapshot = bus.Display.CaptureSnapshot();
Assert.False(hasValue);
Assert.True(snapshot.LastSpriteDmaFetches > 0);
Assert.Equal(ToBgra(0x0F00), Pixel(frame, StandardX, StandardY));
}

[Fact]
public void TimedRenderKeepsLiveSpriteDmaCommandsAfterFrameBoundaryOvershoot()
{
Expand Down Expand Up @@ -1465,6 +1486,15 @@ private static int GetPrivateCollectionCount(object instance, string fieldName)
return value.Count;
}

private static T GetPrivateField<T>(object instance, string fieldName)
{
var field = instance.GetType().GetField(
fieldName,
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
Assert.NotNull(field);
return Assert.IsAssignableFrom<T>(field.GetValue(instance));
}

private static void InvokePrivateMethod(object instance, string methodName, params object[] arguments)
{
var method = instance.GetType().GetMethod(
Expand Down
19 changes: 16 additions & 3 deletions CopperMod.Amiga/AmigaBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1088,16 +1088,23 @@ public uint AddChipDmaPointerOffset(uint pointer, int byteOffset)
}

public PaulaDmaReadResult ReadPaulaDmaWord(uint address, long requestedCycle)
{
return ReadPaulaDmaWord(-1, address, requestedCycle);
}

public PaulaDmaReadResult ReadPaulaDmaWord(int channel, uint address, long requestedCycle)
{
address = MaskChipDmaAddress(address);
var slotChannel = LiveAgnusDmaEnabled ? channel : -1;
var access = Arbitrate(
AmigaBusRequester.Paula,
AmigaBusAccessKind.PaulaDma,
AmigaBusAccessTarget.ChipRam,
address,
AmigaBusAccessSize.Word,
requestedCycle,
isWrite: false);
isWrite: false,
slotChannel);
var value = ReadChipWordForPresentation(address, access.GrantedCycle);

return new PaulaDmaReadResult(value, access);
Expand Down Expand Up @@ -1921,7 +1928,8 @@ private AmigaBusAccessResult Arbitrate(
uint address,
AmigaBusAccessSize size,
long requestedCycle,
bool isWrite)
bool isWrite,
int channel = -1)
{
if (requester == AmigaBusRequester.Cpu &&
target == AmigaBusAccessTarget.CustomRegisters &&
Expand Down Expand Up @@ -1986,7 +1994,8 @@ private AmigaBusAccessResult Arbitrate(
address,
size,
requestedCycle,
isWrite);
isWrite,
channel);
var result = Arbiter.Arbitrate(request);
if (ShouldUseChipSlotScheduler(target))
{
Expand Down Expand Up @@ -2060,6 +2069,10 @@ internal void ClearLiveDisplayDmaSlotsFrom(long cycle)
_hrmSlotEngine.ClearLiveDisplaySlotsFrom(cycle);
}

internal void InvalidateLiveDisplayHrmGrantCache()
{
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private AmigaBusAccessResult ArbitrateChipSlot(AmigaBusAccessRequest request, AmigaBusAccessResult baseResult)
{
Expand Down
52 changes: 39 additions & 13 deletions CopperMod.Amiga/AmigaBusTiming.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ public AmigaBusAccessRequest(
uint address,
AmigaBusAccessSize size,
long requestedCycle,
bool isWrite)
bool isWrite,
int channel = -1)
{
Requester = requester;
Kind = kind;
Expand All @@ -148,6 +149,7 @@ public AmigaBusAccessRequest(
Size = size;
RequestedCycle = requestedCycle;
IsWrite = isWrite;
Channel = channel;
}

public AmigaBusRequester Requester { get; }
Expand All @@ -163,6 +165,8 @@ public AmigaBusAccessRequest(
public long RequestedCycle { get; }

public bool IsWrite { get; }

public int Channel { get; }
}

internal readonly struct AmigaBusAccessResult
Expand Down Expand Up @@ -340,6 +344,8 @@ internal static class AgnusHrmOcsSlotTable
public const int AudioSlotsPerLine = 4;
public const int SpriteSlotsPerLine = 16;
public const int NormalBitplaneSlotsPerLine = 80;
public const int FirstPaulaHorizontal = 0x10;
public const int LastPaulaHorizontal = 0x16;
public const int FirstSpriteHorizontal = 0x18;
public const int LastSpriteHorizontal = 0x36;

Expand Down Expand Up @@ -371,7 +377,9 @@ public static AgnusChipSlotOwner GetFixedOwner(int horizontal)
return AgnusChipSlotOwner.Disk;
}

if (horizontal is 0x10 or 0x12 or 0x14 or 0x16)
if (horizontal >= FirstPaulaHorizontal &&
horizontal <= LastPaulaHorizontal &&
((horizontal - FirstPaulaHorizontal) & 1) == 0)
{
return AgnusChipSlotOwner.Paula;
}
Expand All @@ -391,17 +399,24 @@ public static bool IsMandatoryRefreshSlot(long slotCycle)
return GetFixedOwner(GetHorizontal(slotCycle)) == AgnusChipSlotOwner.Refresh;
}

public static bool IsFixedDmaSlotForOwner(AgnusChipSlotOwner owner, long slotCycle)
public static bool IsFixedDmaSlotForOwner(AgnusChipSlotOwner owner, long slotCycle, int channel = -1)
{
if (owner == AgnusChipSlotOwner.Bitplane)
{
return true;
}

return GetFixedOwner(GetHorizontal(slotCycle)) == owner;
var horizontal = GetHorizontal(slotCycle);
if (owner == AgnusChipSlotOwner.Paula &&
TryGetPaulaHorizontal(channel, out var paulaHorizontal))
{
return horizontal == paulaHorizontal;
}

return GetFixedOwner(horizontal) == owner;
}

public static long FindNextFixedDmaSlot(long requestedCycle, AgnusChipSlotOwner owner)
public static long FindNextFixedDmaSlot(long requestedCycle, AgnusChipSlotOwner owner, int channel = -1)
{
System.Diagnostics.Debug.Assert(requestedCycle >= 0, "Agnus DMA request cycles must be non-negative.");
var candidate = AgnusChipSlotScheduler.AlignToSlot(requestedCycle);
Expand All @@ -413,14 +428,25 @@ public static long FindNextFixedDmaSlot(long requestedCycle, AgnusChipSlotOwner
return candidate;
}

while (!IsFixedDmaSlotForOwner(owner, candidate))
while (!IsFixedDmaSlotForOwner(owner, candidate, channel))
{
candidate += AgnusChipSlotScheduler.SlotCycles;
}

return candidate;
}
}

private static bool TryGetPaulaHorizontal(int channel, out int horizontal)
{
if ((uint)channel < AudioSlotsPerLine)
{
horizontal = FirstPaulaHorizontal + (channel * 2);
return true;
}

horizontal = 0;
return false;
} }

internal sealed class AgnusHrmSlotEngine : IAgnusChipSlotTiming
{
Expand Down Expand Up @@ -557,7 +583,7 @@ public bool TryReserveFixedDmaSlot(AmigaBusAccessRequest request, out AmigaBusAc
}

var owner = GetOwner(request.Requester);
var granted = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(request.RequestedCycle, owner);
var granted = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(request.RequestedCycle, owner, request.Channel);
return TryCommitFixedSlot(request, owner, granted, out result);
}

Expand All @@ -572,7 +598,7 @@ public bool TryReserveExactFixedDmaSlot(AmigaBusAccessRequest request, out Amiga

var owner = GetOwner(request.Requester);
var granted = AgnusChipSlotScheduler.AlignToSlot(request.RequestedCycle);
if (!AgnusHrmOcsSlotTable.IsFixedDmaSlotForOwner(owner, granted))
if (!AgnusHrmOcsSlotTable.IsFixedDmaSlotForOwner(owner, granted, request.Channel))
{
var fixedOwner = AgnusHrmOcsSlotTable.GetFixedOwner(AgnusHrmOcsSlotTable.GetHorizontal(granted));
result = new AmigaBusAccessResult(request, granted, granted);
Expand All @@ -592,15 +618,15 @@ internal bool TryReserveFixedDmaSlotThrough(
long latestGrantCycle,
out AmigaBusAccessResult result)
{
var candidate = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(request.RequestedCycle, owner);
var candidate = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(request.RequestedCycle, owner, request.Channel);
while (candidate <= latestGrantCycle)
{
if (TryCommitFixedSlot(request, owner, candidate, out result))
{
return true;
}

candidate = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(candidate + SlotCycles, owner);
candidate = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(candidate + SlotCycles, owner, request.Channel);
}

result = new AmigaBusAccessResult(request, candidate, candidate);
Expand Down Expand Up @@ -773,11 +799,11 @@ private AmigaBusAccessResult ReserveDeviceFixedDmaSlot(
AmigaBusAccessResult baseResult,
AgnusChipSlotOwner owner)
{
var candidate = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(Math.Max(baseResult.GrantedCycle, request.RequestedCycle), owner);
var candidate = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(Math.Max(baseResult.GrantedCycle, request.RequestedCycle), owner, request.Channel);
AmigaBusAccessResult result;
while (!TryCommitFixedSlot(request, owner, candidate, out result))
{
candidate = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(candidate + SlotCycles, owner);
candidate = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(candidate + SlotCycles, owner, request.Channel);
}

var completed = Math.Max(baseResult.CompletedCycle, result.CompletedCycle);
Expand Down
Loading
Loading