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

View File

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

View File

@ -4,14 +4,21 @@ import jace.Emulator;
import jace.JaceApplication; import jace.JaceApplication;
import jace.core.Computer; import jace.core.Computer;
import jace.core.RAM; import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMListener; import jace.core.RAMListener;
import jace.state.State; import jace.state.State;
import jace.ui.MetacheatUI; 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.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -26,6 +33,60 @@ public class MetaCheat extends Cheats {
NO_CHANGE, ANY_CHANGE, LESS, GREATER, AMOUNT 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 { public static class SearchResult {
int address; int address;
@ -43,11 +104,16 @@ public class MetaCheat extends Cheats {
} }
MetacheatUI ui; MetacheatUI ui;
public int fadeRate = 2;
public int lightRate = 20;
public int historyLength = 10;
private int startAddress = 0; private int startAddress = 0;
private int endAddress = 0x0ffff; private int endAddress = 0x0ffff;
private final StringProperty startAddressProperty = new SimpleStringProperty("0"); private final StringProperty startAddressProperty = new SimpleStringProperty(Integer.toHexString(startAddress));
private final StringProperty endAddressProperty = new SimpleStringProperty("FFFF"); private final StringProperty endAddressProperty = new SimpleStringProperty(Integer.toHexString(endAddress));
private boolean byteSized = false; private boolean byteSized = true;
private SearchType searchType = SearchType.VALUE; private SearchType searchType = SearchType.VALUE;
private SearchChangeType searchChangeType = SearchChangeType.NO_CHANGE; private SearchChangeType searchChangeType = SearchChangeType.NO_CHANGE;
private final BooleanProperty signedProperty = new SimpleBooleanProperty(false); private final BooleanProperty signedProperty = new SimpleBooleanProperty(false);
@ -64,10 +130,10 @@ public class MetaCheat extends Cheats {
addNumericValidator(searchValueProperty); addNumericValidator(searchValueProperty);
addNumericValidator(changeByProperty); addNumericValidator(changeByProperty);
startAddressProperty.addListener((prop, oldVal, newVal) -> { startAddressProperty.addListener((prop, oldVal, newVal) -> {
startAddress = parseInt(newVal); startAddress = Math.max(0, Math.min(65535, parseInt(newVal)));
}); });
endAddressProperty.addListener((prop, oldVal, 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 { public int parseInt(String s) throws NumberFormatException {
if (s == null || s.isEmpty()) {
return 0;
}
if (s.matches("(\\+|-)?[0-9]+")) { if (s.matches("(\\+|-)?[0-9]+")) {
return Integer.parseInt(s); return Integer.parseInt(s);
} else if (s.matches("(\\+|-)?[0-9a-fA-F]+")) { } else if (s.matches("(\\+|-)?[0-9a-fA-F]+")) {
@ -116,10 +185,6 @@ public class MetaCheat extends Cheats {
return "MetaCheat"; return "MetaCheat";
} }
@Override
public void tick() {
}
@Override @Override
public void detach() { public void detach() {
super.detach(); super.detach();
@ -210,7 +275,7 @@ public class MetaCheat extends Cheats {
? signed ? memory.readRaw(result.address) : memory.readRaw(result.address) & 0x0ff ? signed ? memory.readRaw(result.address) : memory.readRaw(result.address) & 0x0ff
: signed ? memory.readWordRaw(result.address) : memory.readWordRaw(result.address) & 0x0ffff; : signed ? memory.readWordRaw(result.address) : memory.readWordRaw(result.address) & 0x0ffff;
int last = result.lastObservedValue; int last = result.lastObservedValue;
result.lastObservedValue = val; result.lastObservedValue = val;
switch (searchType) { switch (searchType) {
case VALUE: case VALUE:
int compare = parseInt(searchValueProperty.get()); 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;
import jace.cheat.MetaCheat.SearchChangeType; import jace.cheat.MetaCheat.SearchChangeType;
import jace.cheat.MetaCheat.SearchType; import jace.cheat.MetaCheat.SearchType;
import jace.core.RAMEvent;
import jace.core.RAMListener; import jace.core.RAMListener;
import jace.state.State; 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.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.control.SingleSelectionModel;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.control.Toggle; import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.TilePane; import javafx.scene.layout.TilePane;
import javafx.scene.paint.Color;
public class MetacheatUI { public class MetacheatUI {
@ -87,6 +97,9 @@ public class MetacheatUI {
@FXML @FXML
private ListView<MetaCheat.SearchResult> searchResultsListView; private ListView<MetaCheat.SearchResult> searchResultsListView;
@FXML
private CheckBox showValuesCheckbox;
@FXML @FXML
private TilePane watchesPane; private TilePane watchesPane;
@ -199,7 +212,6 @@ public class MetacheatUI {
searchTypesTabPane.getTabs().get(1).setUserData(SearchType.CHANGE); searchTypesTabPane.getTabs().get(1).setUserData(SearchType.CHANGE);
searchTypesTabPane.getTabs().get(2).setUserData(SearchType.TEXT); searchTypesTabPane.getTabs().get(2).setUserData(SearchType.TEXT);
searchTypesTabPane.getSelectionModel().selectedItemProperty().addListener((prop, oldVal, newVal) -> { searchTypesTabPane.getSelectionModel().selectedItemProperty().addListener((prop, oldVal, newVal) -> {
System.out.println("Tab selected: " + newVal.getText());
if (cheatEngine != null) { if (cheatEngine != null) {
cheatEngine.setSearchType((SearchType) newVal.getUserData()); cheatEngine.setSearchType((SearchType) newVal.getUserData());
} }
@ -222,7 +234,7 @@ public class MetacheatUI {
if (cheatEngine != null) { if (cheatEngine != null) {
cheatEngine.setByteSized((boolean) newVal.getUserData()); cheatEngine.setByteSized((boolean) newVal.getUserData());
} }
}); });
} }
MetaCheat cheatEngine = null; MetaCheat cheatEngine = null;
@ -239,21 +251,91 @@ public class MetacheatUI {
searchValueField.textProperty().bindBidirectional(cheatEngine.searchValueProperty()); searchValueField.textProperty().bindBidirectional(cheatEngine.searchValueProperty());
searchChangeByField.textProperty().bindBidirectional(cheatEngine.searchChangeByProperty()); 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) { private void changeZoom(double amount) {
double zoom = memoryViewPane.getScaleX(); if (memoryView != null) {
zoom += amount; double zoom = memoryView.getScaleX();
memoryViewPane.setScaleX(zoom); zoom += amount;
memoryViewPane.setScaleY(zoom); memoryView.setScaleX(zoom);
} memoryView.setScaleY(zoom);
StackPane scrollArea = (StackPane) memoryView.getParent();
private void processMemoryEvent(RAMEvent e) { scrollArea.setPrefSize(memoryView.getWidth() * zoom, memoryView.getHeight() * zoom);
if (e.getAddress() < cheatEngine.getStartAddress() || e.getAddress() > cheatEngine.getEndAddress()) {
return;
} }
} }
public void detach() { public void detach() {

View File

@ -14,9 +14,10 @@
<Button mnemonicParsing="false" onAction="#zoomIn" text="Zoom in" /> <Button mnemonicParsing="false" onAction="#zoomIn" text="Zoom in" />
<Button mnemonicParsing="false" onAction="#zoomOut" text="Zoom out" /> <Button mnemonicParsing="false" onAction="#zoomOut" text="Zoom out" />
<Label text="Start:" /> <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:" /> <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> </items>
</ToolBar> </ToolBar>
<SplitPane dividerPositions="0.6304347826086957" prefHeight="363.0" prefWidth="600.0" VBox.vgrow="ALWAYS"> <SplitPane dividerPositions="0.6304347826086957" prefHeight="363.0" prefWidth="600.0" VBox.vgrow="ALWAYS">