From 7dada32cbf8d852680da2e40a5cebf03a46a75f2 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Wed, 19 Aug 2015 17:30:20 -0500 Subject: [PATCH] Memory view is now performance-optimized and a lot of minor UX issues have been resolved. --- src/main/java/jace/apple2e/Apple2e.java | 3 + src/main/java/jace/cheat/Cheats.java | 5 + src/main/java/jace/cheat/MetaCheat.java | 162 ++++++++++++++++++++++-- src/main/java/jace/ui/MetacheatUI.java | 114 ++++++++++++++--- src/main/resources/fxml/Metacheat.fxml | 5 +- 5 files changed, 261 insertions(+), 28 deletions(-) diff --git a/src/main/java/jace/apple2e/Apple2e.java b/src/main/java/jace/apple2e/Apple2e.java index 42c8313..78dcd7f 100644 --- a/src/main/java/jace/apple2e/Apple2e.java +++ b/src/main/java/jace/apple2e/Apple2e.java @@ -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) { diff --git a/src/main/java/jace/cheat/Cheats.java b/src/main/java/jace/cheat/Cheats.java index a39fa5c..c0465c7 100644 --- a/src/main/java/jace/cheat/Cheats.java +++ b/src/main/java/jace/cheat/Cheats.java @@ -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(); diff --git a/src/main/java/jace/cheat/MetaCheat.java b/src/main/java/jace/cheat/MetaCheat.java index c8c28fa..a6051fa 100644 --- a/src/main/java/jace/cheat/MetaCheat.java +++ b/src/main/java/jace/cheat/MetaCheat.java @@ -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{ + + public static ChangeListener 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 readInstructions = FXCollections.observableList(new ArrayList<>()); + public ObservableList writeInstructions = FXCollections.observableList(new ArrayList<>()); + private int x; + private int y; + private int width; + private int height; + + public static void setListener(ChangeListener l) { + listener = l; + } + + public MemoryCell() { + ChangeListener changeListener = (ObservableValue 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 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()); + } + } } diff --git a/src/main/java/jace/ui/MetacheatUI.java b/src/main/java/jace/ui/MetacheatUI.java index 73c648d..4ae9b82 100644 --- a/src/main/java/jace/ui/MetacheatUI.java +++ b/src/main/java/jace/ui/MetacheatUI.java @@ -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 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 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 redrawNodes = new ConcurrentSkipListSet<>(); + ScheduledExecutorService animationTimer = new ScheduledThreadPoolExecutor(1); + ScheduledFuture animationFuture = null; + + private void processMemoryViewUpdates() { + Application.invokeLater(() -> { + GraphicsContext context = memoryView.getGraphicsContext2D(); + Set 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() { diff --git a/src/main/resources/fxml/Metacheat.fxml b/src/main/resources/fxml/Metacheat.fxml index 0fafde8..02fe798 100644 --- a/src/main/resources/fxml/Metacheat.fxml +++ b/src/main/resources/fxml/Metacheat.fxml @@ -14,9 +14,10 @@