InstructionTrace.java

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
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:readsExactlyThreeBytesPerRecordedInstruction()]
negated conditional → KILLED

92

1.1
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:readsExactlyThreeBytesPerRecordedInstruction()]
changed conditional boundary → KILLED

2.2
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:readsExactlyThreeBytesPerRecordedInstruction()]
negated conditional → KILLED

93

1.1
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recordsAllThreeBytesOfTheLongestInstructions()]
Replaced integer addition with subtraction → KILLED

2.2
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recordsAllThreeBytesOfTheLongestInstructions()]
Replaced bitwise AND with OR → KILLED

3.3
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recordsAllThreeBytesOfTheLongestInstructions()]
Replaced integer multiplication with division → KILLED

4.4
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:withoutStateSuppliersHistoryHasZeroedState()]
Replaced bitwise OR with AND → KILLED

5.5
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recordsAllThreeBytesOfTheLongestInstructions()]
Replaced Shift Left with Shift Right → KILLED

6.6
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:withoutStateSuppliersHistoryHasZeroedState()]
Replaced bitwise AND with OR → KILLED

99

1.1
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:readsExactlyThreeBytesPerRecordedInstruction()]
Replaced integer addition with subtraction → KILLED

2.2
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:readsExactlyThreeBytesPerRecordedInstruction()]
Replaced integer modulus with multiplication → KILLED

100

1.1
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:keepsOnlyTheNewestEntriesOldestFirstOnceFull()]
changed conditional boundary → KILLED

2.2
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:exactlyFullIsOldestFirstToo()]
negated conditional → KILLED

101

1.1
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:exactlyFullIsOldestFirstToo()]
Replaced integer addition with subtraction → KILLED

103

1.1
Location : tick
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recordedCountsEveryEntryEvenOnceFull()]
Replaced long addition with subtraction → KILLED

109

1.1
Location : recorded
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recordedCountsEveryEntryEvenOnceFull()]
replaced long return with 0 for com/rox/debug/cpu/InstructionTrace::recorded → KILLED

117

1.1
Location : recent
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:startsEmpty()]
negated conditional → KILLED

2.2
Location : recent
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:startsEmpty()]
changed conditional boundary → KILLED

118

1.1
Location : recent
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:keepsOnlyTheNewestEntriesOldestFirstOnceFull()]
Replaced integer modulus with multiplication → KILLED

2.2
Location : recent
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:keepsOnlyTheNewestEntriesOldestFirstOnceFull()]
Replaced integer addition with subtraction → KILLED

120

1.1
Location : recent
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:startsEmpty()]
replaced return value with null for com/rox/debug/cpu/InstructionTrace::recent → KILLED

125

1.1
Location : recentInstructions
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recordedBytesWrapAtTheTopOfTheAddressSpace()]
replaced return value with Collections.emptyList for com/rox/debug/cpu/InstructionTrace::recentInstructions → KILLED

137

1.1
Location : history
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:withoutStateSuppliersHistoryHasZeroedState()]
negated conditional → KILLED

2.2
Location : history
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recentInstructionsAreOldestFirstAndMatchRecentAddresses()]
changed conditional boundary → KILLED

138

1.1
Location : history
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:historyRecordsTheStateEachInstructionStartedWith()]
Replaced integer addition with subtraction → KILLED

2.2
Location : history
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recentInstructionsAreOldestFirstAndMatchRecentAddresses()]
Replaced integer modulus with multiplication → KILLED

142

1.1
Location : lambda$history$0
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:withoutStateSuppliersHistoryHasZeroedState()]
replaced int return with 0 for com/rox/debug/cpu/InstructionTrace::lambda$history$0 → KILLED

2.2
Location : lambda$history$0
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recordedBytesWrapAtTheTopOfTheAddressSpace()]
Replaced Shift Right with Shift Left → KILLED

3.3
Location : lambda$history$0
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:withoutStateSuppliersHistoryHasZeroedState()]
Replaced bitwise AND with OR → KILLED

4.4
Location : lambda$history$0
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:withoutStateSuppliersHistoryHasZeroedState()]
Replaced bitwise AND with OR → KILLED

5.5
Location : lambda$history$0
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recordedBytesWrapAtTheTopOfTheAddressSpace()]
Replaced integer multiplication with division → KILLED

6.6
Location : lambda$history$0
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:recordedBytesWrapAtTheTopOfTheAddressSpace()]
Replaced integer subtraction with addition → KILLED

146

1.1
Location : history
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:withoutStateSuppliersHistoryHasZeroedState()]
replaced return value with Collections.emptyList for com/rox/debug/cpu/InstructionTrace::history → KILLED

150

1.1
Location : oldestSlot
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:keepsOnlyTheNewestEntriesOldestFirstOnceFull()]
Replaced integer modulus with multiplication → KILLED

2.2
Location : oldestSlot
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:keepsOnlyTheNewestEntriesOldestFirstOnceFull()]
Replaced integer addition with subtraction → KILLED

3.3
Location : oldestSlot
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:withoutStateSuppliersHistoryHasZeroedState()]
Replaced integer subtraction with addition → KILLED

4.4
Location : oldestSlot
Killed by : com.rox.debug.cpu.InstructionTraceTest.[engine:junit-jupiter]/[class:com.rox.debug.cpu.InstructionTraceTest]/[method:keepsOnlyTheNewestEntriesOldestFirstOnceFull()]
replaced int return with 0 for com/rox/debug/cpu/InstructionTrace::oldestSlot → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1