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
2 changes: 1 addition & 1 deletion Copper68k/Copper68k.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<NoWarn>1701;1702;NU1900</NoWarn>
<PackageId>Copper68k</PackageId>
<Title>Copper68k</Title>
<Authors>CopperMod contributors</Authors>
<Authors>Ilkka Lehtoranta</Authors>
<Description>Reusable Motorola 68000-family CPU emulation core with cycle-aware interpreter backends and an opt-in MC68040 JIT.</Description>
<PackageVersion>1.1.0</PackageVersion>
<PackageReleaseNotes>Adds an opt-in internal MC68040 JIT backend through M68kCoreOptions, public JIT bus capability interfaces for host integration, and keeps the default MC68040 factory path on the interpreter for source-compatible package consumers.</PackageReleaseNotes>
Expand Down
18 changes: 6 additions & 12 deletions Copper68k/M68020Interpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4248,24 +4248,18 @@ internal void CompleteTiming(M68kInstructionTimingKey key)
_timing.CompleteInstruction(_timing.GetPlan(key));
}

internal void CompleteTiming(M68kTimingDescriptor descriptor)
{
_timing.CompleteInstruction(descriptor);
}

private void CompleteMovemLongTiming(
M68kInstructionTimingKey key,
string name,
int registerCount,
bool registerToMemory)
{
const int registerListImmediateAddressCycles = 4;
var nativeCycles = _profile.FixedInstructionNativeCycles ?? (_profile.Model == M68kAcceleratorModel.M68030
? registerToMemory
? 4 + (2 * registerCount) + registerListImmediateAddressCycles
: 8 + (4 * registerCount) + registerListImmediateAddressCycles
: registerToMemory
? 4 + (3 * registerCount) + registerListImmediateAddressCycles
: 8 + (4 * registerCount) + registerListImmediateAddressCycles);
var plan = _profile.Model == M68kAcceleratorModel.M68030
? M68kInstructionPlan.CreateHeadTail(key, name, nativeCycles, 2, 0)
: M68kInstructionPlan.CreateFlat(key, name, nativeCycles);
_timing.CompleteInstruction(plan);
CompleteTiming(M68kTimingDescriptor.MovemLong(key, name, registerCount, registerToMemory));
}

private static int CountSetBits(ushort value)
Expand Down
226 changes: 12 additions & 214 deletions Copper68k/M68kTimingEngine.cs

Large diffs are not rendered by default.

635 changes: 635 additions & 0 deletions Copper68k/M68kTimingFormula.cs

Large diffs are not rendered by default.

76 changes: 73 additions & 3 deletions CopperMod.Amiga.Tests/AmigaBusTimingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,19 @@ public void HardwareSchedulerSkipsLiveDisplayAdvanceWhenDisplayHasNoLiveWork()
Assert.True(bus.Agnus.CaptureSnapshot().RefreshSlotReservationCount > 0);
}

[Fact]
public void CpuBoundaryHardwareDrainDoesNotReservePureRefreshSlots()
{
var bus = new AmigaBus(enableLiveAgnusDma: true);
var targetCycle = OutputRowStartCycle(AmigaConstants.PalLowResOverscanBorderY);

bus.AdvanceHardwareEventsTo(targetCycle);
Assert.Equal(0, bus.Agnus.CaptureSnapshot().RefreshSlotReservationCount);

bus.AdvanceHardwareTo(targetCycle);
Assert.True(bus.Agnus.CaptureSnapshot().RefreshSlotReservationCount > 0);
}

[Fact]
public void LiveRasterlinePlanRecordsLineStateEvent()
{
Expand Down Expand Up @@ -559,15 +572,40 @@ public void PredictedRasterlinePlanMatchesSimpleBitplaneLine()
var snapshot = bus.Display.CaptureSnapshot();
Assert.True(
snapshot.LastPredictedRasterlinePlanLines > 0,
$"recorded={snapshot.LastRasterlinePlanEvents}, lineState={snapshot.LastRasterlinePlanLineStateEvents}, bitplane={snapshot.LastRasterlinePlanBitplaneFetchEvents}, lines={snapshot.LastPredictedRasterlinePlanLines}, matched={snapshot.LastPredictedRasterlinePlanMatchedLines}, mismatched={snapshot.LastPredictedRasterlinePlanMismatchedLines}, unsupported={snapshot.LastPredictedRasterlinePlanUnsupportedLines}, copper={snapshot.LastPredictedRasterlinePlanUnsupportedCopperLines}, pending={snapshot.LastPredictedRasterlinePlanUnsupportedPendingWriteLines}, sprite={snapshot.LastPredictedRasterlinePlanUnsupportedSpriteLines}, invalid={snapshot.LastPredictedRasterlinePlanUnsupportedInvalidStateLines}, overflow={snapshot.LastPredictedRasterlinePlanUnsupportedOverflowLines}");
$"recorded={snapshot.LastRasterlinePlanEvents}, lineState={snapshot.LastRasterlinePlanLineStateEvents}, bitplane={snapshot.LastRasterlinePlanBitplaneFetchEvents}, lines={snapshot.LastPredictedRasterlinePlanLines}, matched={snapshot.LastPredictedRasterlinePlanMatchedLines}, mismatched={snapshot.LastPredictedRasterlinePlanMismatchedLines}, unsupported={snapshot.LastPredictedRasterlinePlanUnsupportedLines}, copper={snapshot.LastPredictedRasterlinePlanUnsupportedCopperLines}, pending={snapshot.LastPredictedRasterlinePlanUnsupportedPendingWriteLines}, sprite={snapshot.LastPredictedRasterlinePlanUnsupportedSpriteLines}, invalid={snapshot.LastPredictedRasterlinePlanUnsupportedInvalidStateLines}, overflow={snapshot.LastPredictedRasterlinePlanUnsupportedOverflowLines}, descriptorBuilds={snapshot.LastRasterlineDescriptorBuilds}, descriptorReplayed={snapshot.LastRasterlineDescriptorReplayedRows}, descriptorMismatches={snapshot.LastRasterlineDescriptorMismatches}");
Assert.True(
snapshot.LastPredictedRasterlinePlanMatchedLines > 0,
$"lines={snapshot.LastPredictedRasterlinePlanLines}, matched={snapshot.LastPredictedRasterlinePlanMatchedLines}, mismatched={snapshot.LastPredictedRasterlinePlanMismatchedLines}");
Assert.Equal(0, snapshot.LastPredictedRasterlinePlanMismatchedLines);
Assert.True(snapshot.LastRasterlineDescriptorBuilds > 0);
Assert.True(snapshot.LastRasterlineDescriptorBitplaneRows > 0);
Assert.True(snapshot.LastRasterlineDescriptorReplayAttempts > 0);
Assert.True(snapshot.LastRasterlineDescriptorReplayedRows > 0);
Assert.Equal(0, snapshot.LastRasterlineDescriptorMismatches);
}

[Fact]
public void RasterlineDescriptorReplayReadsChipRamAtFetchTime()
{
var bus = new AmigaBus(enableLiveAgnusDma: true);
var row = AmigaConstants.PalLowResOverscanBorderY;
var lineStart = OutputRowStartCycle(row);
var fetchCycle = LowResPlane1FetchCycle(row);
ConfigureLiveOneBitplaneDma(bus);
BigEndian.WriteUInt16(bus.ChipRam, 0x1000, 0x0000);

bus.AdvanceDmaTo(lineStart);
BigEndian.WriteUInt16(bus.ChipRam, 0x1000, 0xA55A);
bus.AdvanceDmaTo(fetchCycle);

var snapshot = bus.Display.CaptureSnapshot();
Assert.True(snapshot.LastRasterlineDescriptorReplayedRows > 0);
Assert.Equal(0, snapshot.LastRasterlineDescriptorMismatches);
Assert.Equal(0xA55A, ReadLiveBitplaneWord(bus, row, plane: 0, word: 0));
}

[Fact]
public void PredictedRasterlinePlanMarksSpriteLineUnsupported()
public void RasterlineDescriptorReplaysSpriteRows()
{
var bus = new AmigaBus(enableLiveAgnusDma: true);
var lineStart = OutputRowStartCycle(AmigaConstants.PalLowResOverscanBorderY);
Expand All @@ -577,8 +615,12 @@ public void PredictedRasterlinePlanMarksSpriteLineUnsupported()
bus.AdvanceDmaTo(validationStop);

var snapshot = bus.Display.CaptureSnapshot();
Assert.True(snapshot.LastPredictedRasterlinePlanUnsupportedSpriteLines > 0);
Assert.True(snapshot.LastRasterlineDescriptorSpriteRows > 0);
Assert.True(snapshot.LastRasterlineDescriptorReplayAttempts > 0);
Assert.True(snapshot.LastRasterlineDescriptorReplayedRows > 0);
Assert.Equal(0, snapshot.LastPredictedRasterlinePlanUnsupportedSpriteLines);
Assert.Equal(0, snapshot.LastPredictedRasterlinePlanMismatchedLines);
Assert.Equal(0, snapshot.LastRasterlineDescriptorMismatches);
}

[Fact]
Expand Down Expand Up @@ -966,6 +1008,24 @@ public void IntreqTimedReadObservesVblankAtExactCycle()
Assert.NotEqual(0, value & AmigaConstants.IntreqVerticalBlank);
}

[Fact]
public void IntreqReadObservesVblankAfterSameCyclePseudoFastAccess()
{
var bus = new AmigaBus(expansionRamSize: 0x10000);
var frameCycle = (long)AmigaConstants.A500PalCpuCyclesPerFrame;

var memoryCycle = frameCycle;
_ = bus.ReadWord(
AmigaConstants.A500BootPseudoFastRamBase,
ref memoryCycle,
AmigaBusAccessKind.CpuDataRead);

var intreqCycle = frameCycle;
var value = bus.ReadWord(0x00DFF01E, ref intreqCycle, AmigaBusAccessKind.CpuDataRead);

Assert.NotEqual(0, value & AmigaConstants.IntreqVerticalBlank);
}

[Fact]
public void RepeatedIntreqPollingDoesNotMissCiaTimerInterrupt()
{
Expand Down Expand Up @@ -1837,6 +1897,16 @@ private static long LowResPlane1FetchCycle(int row)
=> OutputRowStartCycle(row) +
((0x38 + HrmLowResPlane1FetchSlot) * AgnusChipSlotScheduler.SlotCycles);

private static ushort ReadLiveBitplaneWord(AmigaBus bus, int row, int plane, int word)
{
var field = typeof(OcsDisplay).GetField(
"_liveBitplaneWords",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
Assert.NotNull(field);
var words = Assert.IsType<ushort[]>(field.GetValue(bus.Display));
return words[(row * 6 * 64) + (plane * 64) + word];
}

private static void ConfigureLiveOneBitplaneDma(AmigaBus bus)
{
bus.WriteWord(0x00DFF096, 0x8300);
Expand Down
155 changes: 155 additions & 0 deletions CopperMod.Amiga.Tests/M68kTimingFormulaTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using Copper68k;

namespace CopperMod.Amiga.Tests;

public sealed class M68kTimingFormulaTests
{
public static IEnumerable<object[]> LegacyTimingKeys =>
Enum.GetValues<M68kInstructionTimingKey>()
.Where(key => key is not M68kInstructionTimingKey.MovemLongRegistersToPredecrement and
not M68kInstructionTimingKey.MovemLongPostIncrementToRegisters)
.Select(key => new object[] { key });

[Theory]
[MemberData(nameof(LegacyTimingKeys))]
public void LegacyTimingKeysProduceDescriptorPlansForM68020AndM68030(object keyObject)
{
var key = (M68kInstructionTimingKey)keyObject;
var descriptor = M68kTimingDescriptor.FromLegacyKey(key);

var plan20 = M68kTimingFormula.GetPlan(descriptor, M68kAcceleratorModel.M68020);
var plan30 = M68kTimingFormula.GetPlan(descriptor, M68kAcceleratorModel.M68030);

Assert.Equal(key, descriptor.Key);
Assert.Equal(key, plan20.Key);
Assert.Equal(key, plan30.Key);
Assert.Equal(plan20.Name, plan30.Name);
Assert.Equal(plan20.NativeCycles, plan30.NativeCycles);
Assert.False(plan20.UsesHeadTail);
Assert.True(plan30.UsesHeadTail);
}

[Fact]
public void LegacyMovemKeysRequireRegisterCountDescriptor()
{
Assert.Throws<UnsupportedM68kTimingException>(
() => M68kTimingDescriptor.FromLegacyKey(M68kInstructionTimingKey.MovemLongRegistersToPredecrement));
Assert.Throws<UnsupportedM68kTimingException>(
() => M68kTimingDescriptor.FromLegacyKey(M68kInstructionTimingKey.MovemLongPostIncrementToRegisters));
}

[Fact]
public void DescriptorCapturesSharedMoveOperands()
{
var immediateToDisplacement = M68kTimingDescriptor.FromLegacyKey(M68kInstructionTimingKey.MoveLongImmediateToAddressDisplacement);
Assert.Equal(M68kTimingOperation.Move, immediateToDisplacement.Operation);
Assert.Equal(M68kOperandSize.Long, immediateToDisplacement.Size);
Assert.Equal(M68kTimingOperand.Immediate, immediateToDisplacement.Source);
Assert.Equal(M68kTimingOperand.AddressDisplacement, immediateToDisplacement.Destination);

var indexedToData = M68kTimingDescriptor.FromLegacyKey(M68kInstructionTimingKey.MoveByteBriefIndexedToData);
Assert.Equal(M68kOperandSize.Byte, indexedToData.Size);
Assert.Equal(M68kTimingOperand.BriefIndexed, indexedToData.Source);
Assert.Equal(M68kTimingOperand.DataRegister, indexedToData.Destination);
}

[Fact]
public void M68030FormulaDerivesHeadTailAndModelSpecificBarriers()
{
AssertPlan(
M68kTimingFormula.GetPlan(M68kTimingDescriptor.FromLegacyKey(M68kInstructionTimingKey.MoveLongDataToData), M68kAcceleratorModel.M68030),
nativeCycles: 2,
headCycles: 1,
tailCycles: 1,
M68kTimingBarrier.None);

AssertPlan(
M68kTimingFormula.GetPlan(M68kTimingDescriptor.FromLegacyKey(M68kInstructionTimingKey.LinkLong), M68kAcceleratorModel.M68030),
nativeCycles: 16,
headCycles: 2,
tailCycles: 2,
M68kTimingBarrier.None);

AssertPlan(
M68kTimingFormula.GetPlan(M68kTimingDescriptor.FromLegacyKey(M68kInstructionTimingKey.BranchByteTaken), M68kAcceleratorModel.M68030),
nativeCycles: 6,
headCycles: 0,
tailCycles: 0,
M68kTimingBarrier.FlushPipeline | M68kTimingBarrier.Branch);

AssertPlan(
M68kTimingFormula.GetPlan(M68kTimingDescriptor.FromLegacyKey(M68kInstructionTimingKey.Movec), M68kAcceleratorModel.M68030),
nativeCycles: 12,
headCycles: 0,
tailCycles: 0,
M68kTimingBarrier.CacheControl | M68kTimingBarrier.SynchronizeBus);
}

[Fact]
public void MovemLongFormulaUsesRegisterCountAndModel()
{
var registerToMemory = M68kTimingDescriptor.MovemLong(
M68kInstructionTimingKey.MovemLongRegistersToPredecrement,
"MOVEM.L <list>,-(An)",
registerCount: 3,
registerToMemory: true);
var memoryToRegister = M68kTimingDescriptor.MovemLong(
M68kInstructionTimingKey.MovemLongPostIncrementToRegisters,
"MOVEM.L (An)+,<list>",
registerCount: 3,
registerToMemory: false);

AssertPlan(
M68kTimingFormula.GetPlan(registerToMemory, M68kAcceleratorModel.M68020),
nativeCycles: 17,
usesHeadTail: false);
AssertPlan(
M68kTimingFormula.GetPlan(registerToMemory, M68kAcceleratorModel.M68030),
nativeCycles: 14,
headCycles: 2,
tailCycles: 0,
M68kTimingBarrier.None);
AssertPlan(
M68kTimingFormula.GetPlan(memoryToRegister, M68kAcceleratorModel.M68030),
nativeCycles: 24,
headCycles: 2,
tailCycles: 0,
M68kTimingBarrier.None);
AssertPlan(
M68kTimingFormula.GetPlan(registerToMemory, M68kAcceleratorModel.M68040),
nativeCycles: 17,
usesHeadTail: false);
}

[Fact]
public void FixedCycleProfileOverridesDescriptorFormula()
{
var timing = new M68kTimingEngine(M68020CpuProfile.Ocs68040JitMaxSpeed, new M68kCpuState());
var descriptor = M68kTimingDescriptor.MovemLong(
M68kInstructionTimingKey.MovemLongRegistersToPredecrement,
"MOVEM.L <list>,-(An)",
registerCount: 8,
registerToMemory: true);

var plan = timing.GetPlan(descriptor);

Assert.Equal(1, plan.NativeCycles);
Assert.False(plan.UsesHeadTail);
Assert.Equal("fixed JIT fallback", plan.Name);
}

private static void AssertPlan(
M68kInstructionPlan plan,
int nativeCycles,
int headCycles = 0,
int tailCycles = 0,
M68kTimingBarrier barriers = M68kTimingBarrier.None,
bool usesHeadTail = true)
{
Assert.Equal(nativeCycles, plan.NativeCycles);
Assert.Equal(usesHeadTail, plan.UsesHeadTail);
Assert.Equal(headCycles, plan.HeadCycles);
Assert.Equal(tailCycles, plan.TailCycles);
Assert.Equal(barriers, plan.Barriers);
}
}
18 changes: 16 additions & 2 deletions CopperMod.Amiga/AmigaBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1587,10 +1587,14 @@ public void AdvanceDmaTo(long targetCycle, bool advanceLiveAgnus, bool advancePa
public void AdvanceHardwareTo(long targetCycle)
=> _hardwareScheduler.DrainTo(
targetCycle,
AmigaHardwareEventMask.All | AmigaHardwareEventMask.ForceCatchUp);
AmigaHardwareEventMask.All |
AmigaHardwareEventMask.DiskPassiveInput |
AmigaHardwareEventMask.ForceCatchUp);

public void AdvanceHardwareEventsTo(long targetCycle)
=> _hardwareScheduler.DrainTo(targetCycle, AmigaHardwareEventMask.All);
=> _hardwareScheduler.DrainTo(
targetCycle,
AmigaHardwareEventMask.All | AmigaHardwareEventMask.CpuBoundary);

public AmigaHardwareSchedulerSnapshot CaptureHardwareSchedulerSnapshot()
=> _hardwareScheduler.CaptureSnapshot();
Expand Down Expand Up @@ -1698,6 +1702,16 @@ internal long GetNextAgnusEventCycle(long currentCycle, long targetCycle)
return Agnus.GetNextWakeCandidateCycle(currentCycle, targetCycle, Display.HasLiveDisplayWork());
}

internal long GetNextCpuVisibleAgnusEventCycle(long currentCycle, long targetCycle)
{
if (!LiveAgnusDmaEnabled)
{
return long.MaxValue;
}

return Display.GetNextLiveCopperWakeCandidateCycle(currentCycle, targetCycle) ?? long.MaxValue;
}

private static CustomRegisterReadAdvanceKind GetCustomRegisterReadAdvanceKind(uint customRegisterAddress)
{
var offset = (ushort)(customRegisterAddress & 0x01FE);
Expand Down
Loading
Loading