diff --git a/CopperMod.Amiga.Tests/AmigaSpriteConformanceMatrixTests.cs b/CopperMod.Amiga.Tests/AmigaSpriteConformanceMatrixTests.cs index 84ca552..5c0e225 100644 --- a/CopperMod.Amiga.Tests/AmigaSpriteConformanceMatrixTests.cs +++ b/CopperMod.Amiga.Tests/AmigaSpriteConformanceMatrixTests.cs @@ -155,6 +155,7 @@ private static IEnumerable 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"); @@ -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(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() { @@ -1465,6 +1486,15 @@ private static int GetPrivateCollectionCount(object instance, string fieldName) return value.Count; } + private static T GetPrivateField(object instance, string fieldName) + { + var field = instance.GetType().GetField( + fieldName, + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(field); + return Assert.IsAssignableFrom(field.GetValue(instance)); + } + private static void InvokePrivateMethod(object instance, string methodName, params object[] arguments) { var method = instance.GetType().GetMethod( diff --git a/CopperMod.Amiga/AmigaBus.cs b/CopperMod.Amiga/AmigaBus.cs index 95c64d6..b4f47b9 100644 --- a/CopperMod.Amiga/AmigaBus.cs +++ b/CopperMod.Amiga/AmigaBus.cs @@ -1088,8 +1088,14 @@ 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, @@ -1097,7 +1103,8 @@ public PaulaDmaReadResult ReadPaulaDmaWord(uint address, long requestedCycle) address, AmigaBusAccessSize.Word, requestedCycle, - isWrite: false); + isWrite: false, + slotChannel); var value = ReadChipWordForPresentation(address, access.GrantedCycle); return new PaulaDmaReadResult(value, access); @@ -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 && @@ -1986,7 +1994,8 @@ private AmigaBusAccessResult Arbitrate( address, size, requestedCycle, - isWrite); + isWrite, + channel); var result = Arbiter.Arbitrate(request); if (ShouldUseChipSlotScheduler(target)) { @@ -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) { diff --git a/CopperMod.Amiga/AmigaBusTiming.cs b/CopperMod.Amiga/AmigaBusTiming.cs index 1e9d4ac..930cdec 100644 --- a/CopperMod.Amiga/AmigaBusTiming.cs +++ b/CopperMod.Amiga/AmigaBusTiming.cs @@ -139,7 +139,8 @@ public AmigaBusAccessRequest( uint address, AmigaBusAccessSize size, long requestedCycle, - bool isWrite) + bool isWrite, + int channel = -1) { Requester = requester; Kind = kind; @@ -148,6 +149,7 @@ public AmigaBusAccessRequest( Size = size; RequestedCycle = requestedCycle; IsWrite = isWrite; + Channel = channel; } public AmigaBusRequester Requester { get; } @@ -163,6 +165,8 @@ public AmigaBusAccessRequest( public long RequestedCycle { get; } public bool IsWrite { get; } + + public int Channel { get; } } internal readonly struct AmigaBusAccessResult @@ -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; @@ -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; } @@ -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); @@ -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 { @@ -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); } @@ -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); @@ -592,7 +618,7 @@ 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)) @@ -600,7 +626,7 @@ internal bool TryReserveFixedDmaSlotThrough( return true; } - candidate = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(candidate + SlotCycles, owner); + candidate = AgnusHrmOcsSlotTable.FindNextFixedDmaSlot(candidate + SlotCycles, owner, request.Channel); } result = new AmigaBusAccessResult(request, candidate, candidate); @@ -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); diff --git a/CopperMod.Amiga/OcsDisplay.cs b/CopperMod.Amiga/OcsDisplay.cs index bd803a7..8c1b09c 100644 --- a/CopperMod.Amiga/OcsDisplay.cs +++ b/CopperMod.Amiga/OcsDisplay.cs @@ -153,6 +153,7 @@ internal sealed class OcsDisplay private readonly byte[] _liveSpriteWordMasks = new byte[LowResOutputHeight * LiveSpriteChannelCount]; private readonly bool[] _liveSpriteDmaExhausted = new bool[LiveSpriteChannelCount]; private readonly LiveSpriteDmaState[] _liveSpriteDmaStates = new LiveSpriteDmaState[LiveSpriteChannelCount]; + private SpriteDmaReadLatch _spriteDmaReadLatch; private readonly ushort[] _livePaletteSnapshotColors = new ushort[MaxLivePaletteSnapshots * 32]; private readonly uint[] _livePaletteSnapshotConvertedColors = new uint[MaxLivePaletteSnapshots * PaletteColorCount]; private readonly List _previousLiveSpriteFrameCommands = new(MaxSpriteFrameCommands * 8); @@ -6846,8 +6847,18 @@ private bool TryReadSpriteWordForPresentation( if (!_useTimedPresentationReads && !recordLiveCapture) { - value = _bus.ReadChipWordForPresentation(address); - return true; + _spriteDmaReadLatch = new SpriteDmaReadLatch( + row, + spriteIndex, + word, + _bus.ReadChipWordForPresentation(address), + granted: true, + grantedCycle: 0); + return ConsumeSpriteDmaReadLatch( + ref _spriteDmaReadLatch, + recordDmaFetch: false, + recordLiveCapture: false, + out value); } if (!recordLiveCapture && @@ -6860,37 +6871,77 @@ private bool TryReadSpriteWordForPresentation( if (!IsSpriteDmaSlotAvailable(spriteIndex, word)) { - value = 0; - RecordMissedSpriteDmaSlot(recordLiveCapture); - return false; + _spriteDmaReadLatch = SpriteDmaReadLatch.Denied(row, spriteIndex, word, GetSpriteDmaFetchCycle(row, spriteIndex, word)); + return ConsumeSpriteDmaReadLatch( + ref _spriteDmaReadLatch, + recordDmaFetch: false, + recordLiveCapture, + out value); } var fetchCycle = GetSpriteDmaFetchCycle(row, spriteIndex, word); var alreadyCaptured = recordLiveCapture && _bus.IsHrmChipSlotReserved(fetchCycle); - if (!_bus.TryReadDisplayDmaWordForPresentation( + _spriteDmaReadLatch = LoadSpriteDmaReadLatch(row, spriteIndex, word, address, fetchCycle); + if (!_spriteDmaReadLatch.Granted) + { + return ConsumeSpriteDmaReadLatch( + ref _spriteDmaReadLatch, + recordDmaFetch: false, + recordLiveCapture, + out value); + } + + return ConsumeSpriteDmaReadLatch( + ref _spriteDmaReadLatch, + recordDmaFetch: !alreadyCaptured, + recordLiveCapture, + out value); + } + + private SpriteDmaReadLatch LoadSpriteDmaReadLatch(int row, int spriteIndex, int word, uint address, long fetchCycle) + { + return _bus.TryReadDisplayDmaWordForPresentation( AmigaBusRequester.Sprite, AmigaBusAccessKind.Sprite, address, fetchCycle, - out value, - out var access)) + out var value, + out var access) + ? new SpriteDmaReadLatch(row, spriteIndex, word, value, granted: true, access.GrantedCycle) + : SpriteDmaReadLatch.Denied(row, spriteIndex, word, access.GrantedCycle); + } + + private bool ConsumeSpriteDmaReadLatch( + ref SpriteDmaReadLatch latch, + bool recordDmaFetch, + bool recordLiveCapture, + out ushort value) + { + if (!latch.HasValue || !latch.Granted) { value = 0; - RecordMissedSpriteDmaSlot(recordLiveCapture); + if (latch.HasValue) + { + RecordMissedSpriteDmaSlot(recordLiveCapture); + } + + latch = default; return false; } - if (!alreadyCaptured) + value = latch.Value; + if (recordDmaFetch) { - RecordSpriteDmaFetch(access.GrantedCycle, recordLiveCapture); + RecordSpriteDmaFetch(latch.GrantedCycle, recordLiveCapture); } - LoadSpriteDataRegister(spriteIndex, word, value); + LoadSpriteDataRegister(latch.SpriteIndex, latch.Word, value); if (recordLiveCapture) { - StoreLiveCapturedSpriteWord(row, spriteIndex, word, value); + StoreLiveCapturedSpriteWord(latch.Row, latch.SpriteIndex, latch.Word, value); } + latch = default; return true; } @@ -11315,6 +11366,37 @@ public static BitplaneDmaReadLatch Denied(int row, int plane, int word, long gra public bool HasValue { get; } } + private readonly struct SpriteDmaReadLatch + { + public SpriteDmaReadLatch(int row, int spriteIndex, int word, ushort value, bool granted, long grantedCycle) + { + Row = row; + SpriteIndex = spriteIndex; + Word = word; + Value = value; + Granted = granted; + GrantedCycle = grantedCycle; + HasValue = true; + } + + public static SpriteDmaReadLatch Denied(int row, int spriteIndex, int word, long grantedCycle) + => new SpriteDmaReadLatch(row, spriteIndex, word, 0, granted: false, grantedCycle); + + public int Row { get; } + + public int SpriteIndex { get; } + + public int Word { get; } + + public ushort Value { get; } + + public bool Granted { get; } + + public long GrantedCycle { get; } + + public bool HasValue { get; } + } + private sealed class LiveLineState { public int Generation;