Memory view is now performance-optimized and a lot of minor UX issues have been resolved.

This commit is contained in:
Brendan Robert 2015-08-19 17:30:20 -05:00
parent eec3fdbcf7
commit 7dada32cbf
5 changed files with 261 additions and 28 deletions

View File

@ -314,6 +314,7 @@ public class Apple2e extends Computer {
if (cheatEngine.getValue() == null) {
if (activeCheatEngine != null) {
activeCheatEngine.detach();
motherboard.miscDevices.remove(activeCheatEngine);
}
activeCheatEngine = null;
} else {
@ -324,6 +325,7 @@ public class Apple2e extends Computer {
} else {
activeCheatEngine.detach();
activeCheatEngine = null;
motherboard.miscDevices.remove(activeCheatEngine);
}
}
if (startCheats) {
@ -333,6 +335,7 @@ public class Apple2e extends Computer {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
activeCheatEngine.attach();
motherboard.miscDevices.add(activeCheatEngine);
}
}
} catch (IOException ex) {

View File

@ -89,6 +89,11 @@ public abstract class Cheats extends Device {
listeners.clear();
}
public void removeListener(RAMListener l) {
computer.getMemory().removeListener(l);
listeners.remove(l);
}
@Override
public void reconfigure() {
unregisterListeners();

View File

@ -4,14 +4,21 @@ import jace.Emulator;
import jace.JaceApplication;
import jace.core.Computer;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.state.State;
import jace.ui.MetacheatUI;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@ -26,6 +33,60 @@ public class MetaCheat extends Cheats {
NO_CHANGE, ANY_CHANGE, LESS, GREATER, AMOUNT
}
public static class MemoryCell implements Comparable<MemoryCell>{
public static ChangeListener<MemoryCell> listener;
public int address;
public IntegerProperty value = new SimpleIntegerProperty();
public IntegerProperty readCount = new SimpleIntegerProperty();
public IntegerProperty execCount = new SimpleIntegerProperty();
public IntegerProperty writeCount = new SimpleIntegerProperty();
public ObservableList<Integer> readInstructions = FXCollections.observableList(new ArrayList<>());
public ObservableList<Integer> writeInstructions = FXCollections.observableList(new ArrayList<>());
private int x;
private int y;
private int width;
private int height;
public static void setListener(ChangeListener<MemoryCell> l) {
listener = l;
}
public MemoryCell() {
ChangeListener<Number> changeListener = (ObservableValue<? extends Number> val, Number oldVal, Number newVal) -> {
if (listener != null) {
listener.changed(null, this, this);
}
};
value.addListener(changeListener);
}
public void setRect(int x, int y, int w, int h) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
@Override
public int compareTo(MemoryCell o) {
return address - o.address;
}
}
public static class SearchResult {
int address;
@ -43,11 +104,16 @@ public class MetaCheat extends Cheats {
}
MetacheatUI ui;
public int fadeRate = 2;
public int lightRate = 20;
public int historyLength = 10;
private int startAddress = 0;
private int endAddress = 0x0ffff;
private final StringProperty startAddressProperty = new SimpleStringProperty("0");
private final StringProperty endAddressProperty = new SimpleStringProperty("FFFF");
private boolean byteSized = false;
private final StringProperty startAddressProperty = new SimpleStringProperty(Integer.toHexString(startAddress));
private final StringProperty endAddressProperty = new SimpleStringProperty(Integer.toHexString(endAddress));
private boolean byteSized = true;
private SearchType searchType = SearchType.VALUE;
private SearchChangeType searchChangeType = SearchChangeType.NO_CHANGE;
private final BooleanProperty signedProperty = new SimpleBooleanProperty(false);
@ -64,10 +130,10 @@ public class MetaCheat extends Cheats {
addNumericValidator(searchValueProperty);
addNumericValidator(changeByProperty);
startAddressProperty.addListener((prop, oldVal, newVal) -> {
startAddress = parseInt(newVal);
startAddress = Math.max(0, Math.min(65535, parseInt(newVal)));
});
endAddressProperty.addListener((prop, oldVal, newVal) -> {
endAddress = parseInt(newVal);
endAddress = Math.max(0, Math.min(65535, parseInt(newVal)));
});
}
@ -83,6 +149,9 @@ public class MetaCheat extends Cheats {
}
public int parseInt(String s) throws NumberFormatException {
if (s == null || s.isEmpty()) {
return 0;
}
if (s.matches("(\\+|-)?[0-9]+")) {
return Integer.parseInt(s);
} else if (s.matches("(\\+|-)?[0-9a-fA-F]+")) {
@ -116,10 +185,6 @@ public class MetaCheat extends Cheats {
return "MetaCheat";
}
@Override
public void tick() {
}
@Override
public void detach() {
super.detach();
@ -210,7 +275,7 @@ public class MetaCheat extends Cheats {
? signed ? memory.readRaw(result.address) : memory.readRaw(result.address) & 0x0ff
: signed ? memory.readWordRaw(result.address) : memory.readWordRaw(result.address) & 0x0ffff;
int last = result.lastObservedValue;
result.lastObservedValue = val;
result.lastObservedValue = val;
switch (searchType) {
case VALUE:
int compare = parseInt(searchValueProperty.get());
@ -237,4 +302,81 @@ public class MetaCheat extends Cheats {
});
}
RAMListener memoryViewListener = null;
private final Map<Integer, MemoryCell> memoryCells = new ConcurrentHashMap<>();
public MemoryCell getMemoryCell(int address) {
return memoryCells.get(address);
}
public void initMemoryView() {
RAM memory = Emulator.computer.getMemory();
for (int addr = getStartAddress(); addr <= getEndAddress(); addr++) {
if (getMemoryCell(addr) == null) {
MemoryCell cell = new MemoryCell();
cell.address = addr;
cell.value.set(memory.readRaw(addr));
memoryCells.put(addr, cell);
}
}
if (memoryViewListener == null) {
memoryViewListener = memory.observe(RAMEvent.TYPE.ANY, startAddress, endAddress, this::processMemoryEvent);
listeners.add(memoryViewListener);
}
}
int fadeCounter = 0;
int FADE_TIMER_VALUE = (int) (Emulator.computer.getMotherboard().cyclesPerSecond / 60);
@Override
public void tick() {
if (fadeCounter-- <= 0) {
fadeCounter = FADE_TIMER_VALUE;
memoryCells.values().stream().forEach((cell) -> {
boolean change = false;
if (cell.execCount.get() > 0) {
cell.execCount.set(Math.max(0, cell.execCount.get() - fadeRate));
change = true;
}
if (cell.readCount.get() > 0) {
cell.readCount.set(Math.max(0, cell.readCount.get() - fadeRate));
change = true;
}
if (cell.writeCount.get() > 0) {
cell.writeCount.set(Math.max(0, cell.writeCount.get() - fadeRate));
change = true;
}
if (change) {
cell.listener.changed(null, cell, cell);
}
});
}
}
private void processMemoryEvent(RAMEvent e) {
MemoryCell cell = getMemoryCell(e.getAddress());
if (cell != null) {
int programCounter = Emulator.computer.getCpu().getProgramCounter();
switch (e.getType()) {
case EXECUTE:
case READ_OPERAND:
cell.execCount.set(Math.min(255, cell.execCount.get() + lightRate));
break;
case WRITE:
cell.writeCount.set(Math.min(255, cell.writeCount.get() + lightRate));
cell.writeInstructions.add(programCounter);
if (cell.writeInstructions.size() > historyLength) {
cell.writeInstructions.remove(0);
}
break;
default:
cell.readCount.set(Math.min(255, cell.readCount.get() + lightRate));
cell.readInstructions.add(programCounter);
if (cell.readInstructions.size() > historyLength) {
cell.readInstructions.remove(0);
}
}
cell.value.set(e.getNewValue());
}
}
}

View File

@ -5,28 +5,38 @@ import jace.Emulator;
import jace.cheat.MetaCheat;
import jace.cheat.MetaCheat.SearchChangeType;
import jace.cheat.MetaCheat.SearchType;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.state.State;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SingleSelectionModel;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.TilePane;
import javafx.scene.paint.Color;
public class MetacheatUI {
@ -87,6 +97,9 @@ public class MetacheatUI {
@FXML
private ListView<MetaCheat.SearchResult> searchResultsListView;
@FXML
private CheckBox showValuesCheckbox;
@FXML
private TilePane watchesPane;
@ -199,7 +212,6 @@ public class MetacheatUI {
searchTypesTabPane.getTabs().get(1).setUserData(SearchType.CHANGE);
searchTypesTabPane.getTabs().get(2).setUserData(SearchType.TEXT);
searchTypesTabPane.getSelectionModel().selectedItemProperty().addListener((prop, oldVal, newVal) -> {
System.out.println("Tab selected: " + newVal.getText());
if (cheatEngine != null) {
cheatEngine.setSearchType((SearchType) newVal.getUserData());
}
@ -222,7 +234,7 @@ public class MetacheatUI {
if (cheatEngine != null) {
cheatEngine.setByteSized((boolean) newVal.getUserData());
}
});
});
}
MetaCheat cheatEngine = null;
@ -239,21 +251,91 @@ public class MetacheatUI {
searchValueField.textProperty().bindBidirectional(cheatEngine.searchValueProperty());
searchChangeByField.textProperty().bindBidirectional(cheatEngine.searchChangeByProperty());
engine.addCheat(RAMEvent.TYPE.ANY, this::processMemoryEvent, 0, 0x0ffff);
searchStartAddressField.textProperty().addListener(addressRangeListener);
searchEndAddressField.textProperty().addListener(addressRangeListener);
memoryViewPane.boundsInParentProperty().addListener((prop, oldVal, newVal) -> redrawMemoryView());
Application.invokeLater(this::redrawMemoryView);
}
ChangeListener<String> addressRangeListener = (prop, oldVal, newVal) -> Application.invokeLater(this::redrawMemoryView);
Canvas memoryView = null;
public static final int MEMORY_BOX_SIZE = 4;
public static final int MEMORY_BOX_GAP = 2;
public static final int MEMORY_BOX_TOTAL_SIZE = (MEMORY_BOX_SIZE + MEMORY_BOX_GAP);
public static Set<MetaCheat.MemoryCell> redrawNodes = new ConcurrentSkipListSet<>();
ScheduledExecutorService animationTimer = new ScheduledThreadPoolExecutor(1);
ScheduledFuture animationFuture = null;
private void processMemoryViewUpdates() {
Application.invokeLater(() -> {
GraphicsContext context = memoryView.getGraphicsContext2D();
Set<MetaCheat.MemoryCell> draw = new HashSet<>(redrawNodes);
redrawNodes.clear();
draw.stream().forEach((cell) -> {
if (showValuesCheckbox.isSelected()) {
int val = cell.value.get() & 0x0ff;
context.setFill(Color.rgb(val, val, val));
} else {
context.setFill(Color.rgb(
cell.writeCount.get(),
cell.readCount.get(),
cell.execCount.get()));
}
context.fillRect(cell.getX(), cell.getY(), cell.getWidth(), cell.getHeight());
});
});
}
public void redrawMemoryView() {
boolean resume = Emulator.computer.pause();
if (memoryViewPane.getContent() != null && memoryView != null) {
memoryViewPane.setContent(null);
}
if (animationFuture != null) {
animationFuture.cancel(false);
}
animationFuture = animationTimer.scheduleAtFixedRate(this::processMemoryViewUpdates, 1000 / 60, 1000 / 60, TimeUnit.MILLISECONDS);
cheatEngine.initMemoryView();
int pixelsPerBlock = 16 * MEMORY_BOX_TOTAL_SIZE;
int cols = ((int) memoryViewPane.getWidth()) / pixelsPerBlock * 16;
int rows = ((cheatEngine.getEndAddress() - cheatEngine.getStartAddress()) / cols) + 1;
memoryView = new Canvas(memoryViewPane.getWidth(), rows * MEMORY_BOX_TOTAL_SIZE);
StackPane pane = new StackPane(memoryView);
memoryViewPane.setContent(pane);
GraphicsContext context = memoryView.getGraphicsContext2D();
context.setFill(Color.rgb(40, 40, 40));
context.fillRect(0, 0, memoryView.getWidth(), memoryView.getHeight());
for (int addr = cheatEngine.getStartAddress(); addr <= cheatEngine.getEndAddress(); addr++) {
int col = (addr - cheatEngine.getStartAddress()) % cols;
int row = (addr - cheatEngine.getStartAddress()) / cols;
MetaCheat.MemoryCell cell = cheatEngine.getMemoryCell(addr);
cell.setRect(col * MEMORY_BOX_TOTAL_SIZE, row * MEMORY_BOX_TOTAL_SIZE, MEMORY_BOX_SIZE, MEMORY_BOX_SIZE);
redrawNodes.add(cell);
}
MetaCheat.MemoryCell.setListener((prop, oldCell, newCell) -> {
redrawNodes.add(newCell);
});
if (resume) {
Emulator.computer.resume();
}
}
private void changeZoom(double amount) {
double zoom = memoryViewPane.getScaleX();
zoom += amount;
memoryViewPane.setScaleX(zoom);
memoryViewPane.setScaleY(zoom);
}
private void processMemoryEvent(RAMEvent e) {
if (e.getAddress() < cheatEngine.getStartAddress() || e.getAddress() > cheatEngine.getEndAddress()) {
return;
if (memoryView != null) {
double zoom = memoryView.getScaleX();
zoom += amount;
memoryView.setScaleX(zoom);
memoryView.setScaleY(zoom);
StackPane scrollArea = (StackPane) memoryView.getParent();
scrollArea.setPrefSize(memoryView.getWidth() * zoom, memoryView.getHeight() * zoom);
}
}
public void detach() {

View File

@ -14,9 +14,10 @@
<Button mnemonicParsing="false" onAction="#zoomIn" text="Zoom in" />
<Button mnemonicParsing="false" onAction="#zoomOut" text="Zoom out" />
<Label text="Start:" />
<TextField fx:id="searchStartAddressField" prefHeight="26.0" prefWidth="50.0" text="0000" />
<TextField fx:id="searchStartAddressField" prefHeight="26.0" prefWidth="60.0" text="0000" />
<Label text="End:" />
<TextField fx:id="searchEndAddressField" prefHeight="26.0" prefWidth="48.0" text="FFFF" />
<TextField fx:id="searchEndAddressField" prefHeight="26.0" prefWidth="60.0" text="FFFF" />
<CheckBox fx:id="showValuesCheckbox" mnemonicParsing="false" text="Show Values" />
</items>
</ToolBar>
<SplitPane dividerPositions="0.6304347826086957" prefHeight="363.0" prefWidth="600.0" VBox.vgrow="ALWAYS">