| 1 | package com.rox.debug.cpu; | |
| 2 | ||
| 3 | import com.rox.clock.ClockWatcher; | |
| 4 | import com.rox.cpu.mos6502.MOS6502Snapshot; | |
| 5 | import com.rox.cpu.mos6502.assembler.DisassembledInstruction; | |
| 6 | import com.rox.cpu.mos6502.assembler.Disassembler; | |
| 7 | ||
| 8 | import java.util.ArrayList; | |
| 9 | import java.util.List; | |
| 10 | import java.util.function.BooleanSupplier; | |
| 11 | import java.util.function.IntSupplier; | |
| 12 | import java.util.function.IntUnaryOperator; | |
| 13 | ||
| 14 | /** | |
| 15 | * A rolling history of where the CPU has been: after every tick, if the CPU is between instructions, | |
| 16 | * the address of the instruction it's about to start - and its bytes, read there and then - are | |
| 17 | * recorded into a fixed-size ring buffer. This is the "how did we get here" a forward-only disassembler | |
| 18 | * can't give (6502 code can't be decoded backwards). The newest entry is therefore the current PC | |
| 19 | * whenever the CPU is between instructions. | |
| 20 | * | |
| 21 | * <p>The bytes are kept, rather than re-read when displayed, because the same address can hold | |
| 22 | * different code later: a mapper bank switch (common - MMC1/MMC3 games swap PRG-ROM banks constantly) | |
| 23 | * or self-modifying code in RAM would otherwise show history as instructions that never ran. | |
| 24 | * | |
| 25 | * <p>Must be ticked <em>after</em> anything that can start a DMA stall on the same tick, so a stall | |
| 26 | * holds recording back until it ends rather than the same address being seen twice. {@link #tick()} | |
| 27 | * runs on the clock thread; {@link #recent()} may be read from another thread for display, so a live | |
| 28 | * read is best-effort - exact while the clock is paused. | |
| 29 | */ | |
| 30 | public final class InstructionTrace implements ClockWatcher { | |
| 31 | public static final int DEFAULT_CAPACITY = 32; | |
| 32 | /** The longest 6502 instruction: opcode plus a 2-byte operand. */ | |
| 33 | private static final int MAX_INSTRUCTION_BYTES = 3; | |
| 34 | private static final int ADDRESS_MASK = 0xFFFF; | |
| 35 | private static final int BYTE_MASK = 0xFF; | |
| 36 | private static final int BITS_PER_BYTE = 8; | |
| 37 | ||
| 38 | private final BooleanSupplier atInstructionBoundary; | |
| 39 | private final IntSupplier programCounter; | |
| 40 | private final IntUnaryOperator peekByte; | |
| 41 | private final IntSupplier packedRegisters; | |
| 42 | private final IntSupplier packedFlags; | |
| 43 | private final int[] addresses; | |
| 44 | private final int[] packedBytes; //up to MAX_INSTRUCTION_BYTES per entry, first byte lowest | |
| 45 | private final int[] registers; //per MOS6502Snapshot.packRegisters | |
| 46 | private final int[] flags; //per MOS6502Snapshot.packFlags | |
| 47 | private int nextSlot; | |
| 48 | private int filled; | |
| 49 | /** Every entry ever recorded, so it keeps counting once the buffer is full. */ | |
| 50 | private volatile long recorded; | |
| 51 | ||
| 52 | /** | |
| 53 | * @param atInstructionBoundary e.g. {@code cpu::isAtInstructionBoundary} | |
| 54 | * @param programCounter e.g. {@code cpu::programCounter} | |
| 55 | * @param peekByte reads a byte the CPU could fetch, without side effects - called on the clock thread | |
| 56 | */ | |
| 57 | public InstructionTrace(final BooleanSupplier atInstructionBoundary, final IntSupplier programCounter, | |
| 58 | final IntUnaryOperator peekByte){ | |
| 59 | this(atInstructionBoundary, programCounter, peekByte, DEFAULT_CAPACITY); | |
| 60 | } | |
| 61 | ||
| 62 | public InstructionTrace(final BooleanSupplier atInstructionBoundary, final IntSupplier programCounter, | |
| 63 | final IntUnaryOperator peekByte, final int capacity){ | |
| 64 | this(atInstructionBoundary, programCounter, peekByte, () -> 0, () -> 0, capacity); | |
| 65 | } | |
| 66 | ||
| 67 | /** | |
| 68 | * Also records the processor state each instruction starts with - see {@link #history()}. | |
| 69 | * | |
| 70 | * @param packedRegisters e.g. {@code cpu::packedRegisters} | |
| 71 | * @param packedFlags e.g. {@code cpu::packedFlags} | |
| 72 | */ | |
| 73 | public InstructionTrace(final BooleanSupplier atInstructionBoundary, final IntSupplier programCounter, | |
| 74 | final IntUnaryOperator peekByte, final IntSupplier packedRegisters, | |
| 75 | final IntSupplier packedFlags, final int capacity){ | |
| 76 | this.atInstructionBoundary = atInstructionBoundary; | |
| 77 | this.programCounter = programCounter; | |
| 78 | this.peekByte = peekByte; | |
| 79 | this.packedRegisters = packedRegisters; | |
| 80 | this.packedFlags = packedFlags; | |
| 81 | this.addresses = new int[capacity]; | |
| 82 | this.packedBytes = new int[capacity]; | |
| 83 | this.registers = new int[capacity]; | |
| 84 | this.flags = new int[capacity]; | |
| 85 | } | |
| 86 | ||
| 87 | @Override | |
| 88 | public void tick(){ | |
| 89 |
1
1. tick : negated conditional → KILLED |
if (atInstructionBoundary.getAsBoolean()){ |
| 90 | final int address = programCounter.getAsInt(); | |
| 91 | int bytes = 0; | |
| 92 |
2
1. tick : changed conditional boundary → KILLED 2. tick : negated conditional → KILLED |
for (int i = 0; i < MAX_INSTRUCTION_BYTES; i++){ |
| 93 |
6
1. tick : Replaced integer addition with subtraction → KILLED 2. tick : Replaced bitwise AND with OR → KILLED 3. tick : Replaced integer multiplication with division → KILLED 4. tick : Replaced bitwise OR with AND → KILLED 5. tick : Replaced Shift Left with Shift Right → KILLED 6. tick : Replaced bitwise AND with OR → KILLED |
bytes |= (peekByte.applyAsInt((address + i) & ADDRESS_MASK) & BYTE_MASK) << (i * BITS_PER_BYTE); |
| 94 | } | |
| 95 | addresses[nextSlot] = address; | |
| 96 | packedBytes[nextSlot] = bytes; | |
| 97 | registers[nextSlot] = packedRegisters.getAsInt(); | |
| 98 | flags[nextSlot] = packedFlags.getAsInt(); | |
| 99 |
2
1. tick : Replaced integer addition with subtraction → KILLED 2. tick : Replaced integer modulus with multiplication → KILLED |
nextSlot = (nextSlot + 1) % addresses.length; |
| 100 |
2
1. tick : changed conditional boundary → KILLED 2. tick : negated conditional → KILLED |
if (filled < addresses.length){ |
| 101 |
1
1. tick : Replaced integer addition with subtraction → KILLED |
filled++; |
| 102 | } | |
| 103 |
1
1. tick : Replaced long addition with subtraction → KILLED |
recorded++; |
| 104 | } | |
| 105 | } | |
| 106 | ||
| 107 | /** How many instructions have been recorded in total - more than {@link #recent()} holds once it's full. */ | |
| 108 | public long recorded(){ | |
| 109 |
1
1. recorded : replaced long return with 0 for com/rox/debug/cpu/InstructionTrace::recorded → KILLED |
return recorded; |
| 110 | } | |
| 111 | ||
| 112 | /** The recorded instruction addresses, oldest first - at most the buffer's capacity. */ | |
| 113 | public int[] recent(){ | |
| 114 | final int count = filled; | |
| 115 | final int[] result = new int[count]; | |
| 116 | final int oldest = oldestSlot(count); | |
| 117 |
2
1. recent : negated conditional → KILLED 2. recent : changed conditional boundary → KILLED |
for (int i = 0; i < count; i++){ |
| 118 |
2
1. recent : Replaced integer modulus with multiplication → KILLED 2. recent : Replaced integer addition with subtraction → KILLED |
result[i] = addresses[(oldest + i) % addresses.length]; |
| 119 | } | |
| 120 |
1
1. recent : replaced return value with null for com/rox/debug/cpu/InstructionTrace::recent → KILLED |
return result; |
| 121 | } | |
| 122 | ||
| 123 | /** The recorded instructions, oldest first, decoded from the bytes they had when they ran. */ | |
| 124 | public List<DisassembledInstruction> recentInstructions(){ | |
| 125 |
1
1. recentInstructions : replaced return value with Collections.emptyList for com/rox/debug/cpu/InstructionTrace::recentInstructions → KILLED |
return history().stream().map(TracedInstruction::instruction).toList(); |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * {@link #recentInstructions()} with the processor state each one started with, oldest first. Each entry's | |
| 130 | * instruction and state come from the same slot in one pass, so they can't drift apart if the clock | |
| 131 | * thread records more meanwhile - though while running, a slot being rewritten can still read half-old. | |
| 132 | */ | |
| 133 | public List<TracedInstruction> history(){ | |
| 134 | final int count = filled; | |
| 135 | final int oldest = oldestSlot(count); | |
| 136 | final List<TracedInstruction> result = new ArrayList<>(count); | |
| 137 |
2
1. history : negated conditional → KILLED 2. history : changed conditional boundary → KILLED |
for (int i = 0; i < count; i++){ |
| 138 |
2
1. history : Replaced integer addition with subtraction → KILLED 2. history : Replaced integer modulus with multiplication → KILLED |
final int slot = (oldest + i) % addresses.length; |
| 139 | final int address = addresses[slot]; | |
| 140 | final int bytes = packedBytes[slot]; | |
| 141 | final DisassembledInstruction instruction = Disassembler.disassembleOne( | |
| 142 |
6
1. lambda$history$0 : replaced int return with 0 for com/rox/debug/cpu/InstructionTrace::lambda$history$0 → KILLED 2. lambda$history$0 : Replaced Shift Right with Shift Left → KILLED 3. lambda$history$0 : Replaced bitwise AND with OR → KILLED 4. lambda$history$0 : Replaced bitwise AND with OR → KILLED 5. lambda$history$0 : Replaced integer multiplication with division → KILLED 6. lambda$history$0 : Replaced integer subtraction with addition → KILLED |
peekAddress -> (bytes >> (((peekAddress - address) & ADDRESS_MASK) * BITS_PER_BYTE)) & BYTE_MASK, |
| 143 | address); | |
| 144 | result.add(new TracedInstruction(instruction, MOS6502Snapshot.unpack(address, registers[slot], flags[slot]))); | |
| 145 | } | |
| 146 |
1
1. history : replaced return value with Collections.emptyList for com/rox/debug/cpu/InstructionTrace::history → KILLED |
return result; |
| 147 | } | |
| 148 | ||
| 149 | private int oldestSlot(final int count){ | |
| 150 |
4
1. oldestSlot : Replaced integer modulus with multiplication → KILLED 2. oldestSlot : Replaced integer addition with subtraction → KILLED 3. oldestSlot : Replaced integer subtraction with addition → KILLED 4. oldestSlot : replaced int return with 0 for com/rox/debug/cpu/InstructionTrace::oldestSlot → KILLED |
return (nextSlot - count + addresses.length) % addresses.length; |
| 151 | } | |
| 152 | } | |
Mutations | ||
| 89 |
1.1 |
|
| 92 |
1.1 2.2 |
|
| 93 |
1.1 2.2 3.3 4.4 5.5 6.6 |
|
| 99 |
1.1 2.2 |
|
| 100 |
1.1 2.2 |
|
| 101 |
1.1 |
|
| 103 |
1.1 |
|
| 109 |
1.1 |
|
| 117 |
1.1 2.2 |
|
| 118 |
1.1 2.2 |
|
| 120 |
1.1 |
|
| 125 |
1.1 |
|
| 137 |
1.1 2.2 |
|
| 138 |
1.1 2.2 |
|
| 142 |
1.1 2.2 3.3 4.4 5.5 6.6 |
|
| 146 |
1.1 |
|
| 150 |
1.1 2.2 3.3 4.4 |