MemoryEditor.java

1
package com.rox.debug.memory;
2
3
import com.rox.clock.ClockWatcher;
4
5
import java.util.Map;
6
import java.util.Queue;
7
import java.util.TreeMap;
8
import java.util.concurrent.ConcurrentHashMap;
9
import java.util.concurrent.ConcurrentLinkedQueue;
10
11
/**
12
 * Debugging edits to memory: one-off writes, and locks that keep a byte at a value. Only CPU RAM
13
 * ({@code $0000-$1FFF}, mirrored down to its real 2KB) and PRG-RAM ({@code $6000-$7FFF}) can be edited -
14
 * anywhere else, a write would hit hardware registers or a mapper's bank switching rather than memory.
15
 *
16
 * <p>While the emulation runs, edits are {@link #queue}d and applied on the clock thread by {@link #tick()},
17
 * so the UI never writes into memory the emulation is using; with nothing ticking, they're
18
 * {@link #write}n straight away. Locks are re-applied every tick, after anything queued, so a locked byte
19
 * holds its value whatever the game writes. With nothing queued or locked, a tick costs one volatile read.
20
 */
21
public final class MemoryEditor implements ClockWatcher {
22
    /** Where an edit actually lands - CPU RAM by its real {@code $0000-$07FF} address, or PRG-RAM by CPU address. */
23
    @FunctionalInterface
24
    public interface ByteWriter {
25
        void write(int address, int value);
26
    }
27
28
    private static final int CPU_RAM_END = 0x1FFF;
29
    private static final int CPU_RAM_MIRROR_MASK = 0x07FF;
30
    private static final int PRG_RAM_START = 0x6000;
31
    private static final int PRG_RAM_END = 0x7FFF;
32
    private static final int MAX_BYTE = 0xFF;
33
34
    private record Edit(int address, int value){
35
    }
36
37
    private final ByteWriter ramWriter;
38
    private final ByteWriter prgRamWriter;
39
    private final Queue<Edit> queued = new ConcurrentLinkedQueue<>();
40
    private final Map<Integer, Integer> locks = new ConcurrentHashMap<>();
41
    //set whenever there's anything to apply; tick() re-checks after clearing it, so an edit queued
42
    //concurrently is never left waiting
43
    private volatile boolean workPending;
44
45
    public MemoryEditor(final ByteWriter ramWriter, final ByteWriter prgRamWriter){
46
        this.ramWriter = ramWriter;
47
        this.prgRamWriter = prgRamWriter;
48
    }
49
50
    /** True for CPU RAM ({@code $0000-$1FFF}) and PRG-RAM ({@code $6000-$7FFF}). */
51
    public static boolean isEditable(final int address){
52 9 1. isEditable : replaced boolean return with true for com/rox/debug/memory/MemoryEditor::isEditable → KILLED
2. isEditable : changed conditional boundary → KILLED
3. isEditable : negated conditional → KILLED
4. isEditable : changed conditional boundary → KILLED
5. isEditable : negated conditional → KILLED
6. isEditable : changed conditional boundary → KILLED
7. isEditable : negated conditional → KILLED
8. isEditable : changed conditional boundary → KILLED
9. isEditable : negated conditional → KILLED
        return (address >= 0 && address <= CPU_RAM_END) || (address >= PRG_RAM_START && address <= PRG_RAM_END);
53
    }
54
55
    /** Writes now - only when nothing else is ticking (paused, or not running). */
56
    public void write(final int address, final int value){
57
        final int canonical = validate(address, value);
58 1 1. write : removed call to com/rox/debug/memory/MemoryEditor::apply → KILLED
        apply(canonical, value);
59
    }
60
61
    /** Writes at the next {@link #tick()}, on the clock thread. */
62
    public void queue(final int address, final int value){
63
        queued.add(new Edit(validate(address, value), value));
64
        workPending = true;
65
    }
66
67
    /** Keeps {@code address} at {@code value} from the next tick on, until {@link #unlock}ed. Replaces any existing lock there. */
68
    public void lock(final int address, final int value){
69
        locks.put(validate(address, value), value);
70
        workPending = true;
71
    }
72
73
    public void unlock(final int address){
74
        locks.remove(canonical(address));
75
    }
76
77
    /** Current locks, address to value, in address order (RAM by its real {@code $0000-$07FF} address). */
78
    public Map<Integer, Integer> locks(){
79 1 1. locks : replaced return value with Collections.emptyMap for com/rox/debug/memory/MemoryEditor::locks → KILLED
        return new TreeMap<>(locks);
80
    }
81
82
    /** Whether the next {@link #tick()} has anything to do - false whenever nothing is queued or locked, keeping idle ticks cheap. */
83
    boolean hasPendingWork(){
84 2 1. hasPendingWork : replaced boolean return with false for com/rox/debug/memory/MemoryEditor::hasPendingWork → KILLED
2. hasPendingWork : replaced boolean return with true for com/rox/debug/memory/MemoryEditor::hasPendingWork → KILLED
        return workPending;
85
    }
86
87
    @Override
88
    public void tick(){
89 1 1. tick : negated conditional → KILLED
        if (!workPending){
90
            return;
91
        }
92
        workPending = false;
93
        Edit edit;
94 1 1. tick : negated conditional → KILLED
        while ((edit = queued.poll()) != null){
95 1 1. tick : removed call to com/rox/debug/memory/MemoryEditor::apply → KILLED
            apply(edit.address(), edit.value());
96
        }
97 1 1. tick : removed call to java/util/Map::forEach → KILLED
        locks.forEach(this::apply);
98 2 1. tick : negated conditional → KILLED
2. tick : negated conditional → KILLED
        if (!locks.isEmpty() || !queued.isEmpty()){
99
            workPending = true;
100
        }
101
    }
102
103
    private int validate(final int address, final int value){
104 1 1. validate : negated conditional → KILLED
        if (!isEditable(address)){
105
            throw new IllegalArgumentException(String.format("$%04X can't be edited - only CPU RAM ($0000-$1FFF) and PRG-RAM ($6000-$7FFF)", address));
106
        }
107 4 1. validate : negated conditional → KILLED
2. validate : changed conditional boundary → KILLED
3. validate : negated conditional → KILLED
4. validate : changed conditional boundary → KILLED
        if (value < 0 || value > MAX_BYTE){
108
            throw new IllegalArgumentException("A byte is 0-255, not " + value);
109
        }
110 1 1. validate : replaced int return with 0 for com/rox/debug/memory/MemoryEditor::validate → KILLED
        return canonical(address);
111
    }
112
113
    /** RAM mirrors reduced to the real byte, so $0810 and $0010 are the same edit/lock - how {@link #locks()} keys its addresses. */
114
    public static int canonical(final int address){
115 4 1. canonical : changed conditional boundary → KILLED
2. canonical : replaced int return with 0 for com/rox/debug/memory/MemoryEditor::canonical → KILLED
3. canonical : Replaced bitwise AND with OR → KILLED
4. canonical : negated conditional → KILLED
        return address <= CPU_RAM_END ? address & CPU_RAM_MIRROR_MASK : address;
116
    }
117
118
    /** Takes a {@link #canonical} address: CPU RAM is already down at $0000-$07FF by now. */
119
    private void apply(final int address, final int value){
120 2 1. apply : negated conditional → KILLED
2. apply : changed conditional boundary → KILLED
        if (address < PRG_RAM_START){
121 1 1. apply : removed call to com/rox/debug/memory/MemoryEditor$ByteWriter::write → KILLED
            ramWriter.write(address, value);
122
        } else {
123 1 1. apply : removed call to com/rox/debug/memory/MemoryEditor$ByteWriter::write → KILLED
            prgRamWriter.write(address, value);
124
        }
125
    }
126
}

Mutations

52

1.1
Location : isEditable
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[test-template:registersRomAndUnmappedSpaceAreNot(int)]/[test-template-invocation:#7]
replaced boolean return with true for com/rox/debug/memory/MemoryEditor::isEditable → KILLED

2.2
Location : isEditable
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[test-template:cpuRamAndPrgRamAreEditable(int)]/[test-template-invocation:#6]
changed conditional boundary → KILLED

3.3
Location : isEditable
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[test-template:registersRomAndUnmappedSpaceAreNot(int)]/[test-template-invocation:#6]
negated conditional → KILLED

4.4
Location : isEditable
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[test-template:cpuRamAndPrgRamAreEditable(int)]/[test-template-invocation:#4]
changed conditional boundary → KILLED

5.5
Location : isEditable
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[test-template:cpuRamAndPrgRamAreEditable(int)]/[test-template-invocation:#2]
negated conditional → KILLED

6.6
Location : isEditable
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[test-template:cpuRamAndPrgRamAreEditable(int)]/[test-template-invocation:#5]
changed conditional boundary → KILLED

7.7
Location : isEditable
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[test-template:cpuRamAndPrgRamAreEditable(int)]/[test-template-invocation:#6]
negated conditional → KILLED

8.8
Location : isEditable
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[test-template:cpuRamAndPrgRamAreEditable(int)]/[test-template-invocation:#1]
changed conditional boundary → KILLED

9.9
Location : isEditable
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[test-template:cpuRamAndPrgRamAreEditable(int)]/[test-template-invocation:#2]
negated conditional → KILLED

58

1.1
Location : write
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:prgRamStartsExactlyAt6000()]
removed call to com/rox/debug/memory/MemoryEditor::apply → KILLED

79

1.1
Location : locks
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:locksIsACopy()]
replaced return value with Collections.emptyMap for com/rox/debug/memory/MemoryEditor::locks → KILLED

84

1.1
Location : hasPendingWork
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:pendingWorkClearsOnceQueuedEditsAreDoneAndOnlyStaysWhileLocked()]
replaced boolean return with false for com/rox/debug/memory/MemoryEditor::hasPendingWork → KILLED

2.2
Location : hasPendingWork
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:pendingWorkClearsOnceQueuedEditsAreDoneAndOnlyStaysWhileLocked()]
replaced boolean return with true for com/rox/debug/memory/MemoryEditor::hasPendingWork → KILLED

89

1.1
Location : tick
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:lockingAgainReplacesTheValue()]
negated conditional → KILLED

94

1.1
Location : tick
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:lockingAgainReplacesTheValue()]
negated conditional → KILLED

95

1.1
Location : tick
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:withNothingToDoATickWritesNothing()]
removed call to com/rox/debug/memory/MemoryEditor::apply → KILLED

97

1.1
Location : tick
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:lockingAgainReplacesTheValue()]
removed call to java/util/Map::forEach → KILLED

98

1.1
Location : tick
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:locksApplyEveryTickUntilUnlocked()]
negated conditional → KILLED

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

104

1.1
Location : validate
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:prgRamStartsExactlyAt6000()]
negated conditional → KILLED

107

1.1
Location : validate
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:prgRamStartsExactlyAt6000()]
negated conditional → KILLED

2.2
Location : validate
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:editsQueuedFromAnotherThreadAllArrive()]
changed conditional boundary → KILLED

3.3
Location : validate
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:prgRamStartsExactlyAt6000()]
negated conditional → KILLED

4.4
Location : validate
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:refusesUneditableAddressesAndNonByteValues()]
changed conditional boundary → KILLED

110

1.1
Location : validate
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:prgRamStartsExactlyAt6000()]
replaced int return with 0 for com/rox/debug/memory/MemoryEditor::validate → KILLED

115

1.1
Location : canonical
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:theLastRamMirrorByteIsTheLastRealRamByte()]
changed conditional boundary → KILLED

2.2
Location : canonical
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:prgRamStartsExactlyAt6000()]
replaced int return with 0 for com/rox/debug/memory/MemoryEditor::canonical → KILLED

3.3
Location : canonical
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:ramMirrorsAreWrittenToTheRealByte()]
Replaced bitwise AND with OR → KILLED

4.4
Location : canonical
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:prgRamStartsExactlyAt6000()]
negated conditional → KILLED

120

1.1
Location : apply
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:prgRamStartsExactlyAt6000()]
negated conditional → KILLED

2.2
Location : apply
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:prgRamStartsExactlyAt6000()]
changed conditional boundary → KILLED

121

1.1
Location : apply
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:ramMirrorsAreWrittenToTheRealByte()]
removed call to com/rox/debug/memory/MemoryEditor$ByteWriter::write → KILLED

123

1.1
Location : apply
Killed by : com.rox.debug.memory.MemoryEditorTest.[engine:junit-jupiter]/[class:com.rox.debug.memory.MemoryEditorTest]/[method:prgRamStartsExactlyAt6000()]
removed call to com/rox/debug/memory/MemoryEditor$ByteWriter::write → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1