lawless-legends/Platform/Apple/tools/jace/src/main/java/jace/state/StateManager.java

436 lines
15 KiB
Java

/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.state;
import java.awt.image.BufferedImage;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.Emulator;
import jace.apple2e.SoftSwitches;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import jace.config.Reconfigurable;
import jace.core.Computer;
import jace.core.PagedMemory;
import jace.core.Video;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class StateManager implements Reconfigurable {
private static StateManager instance;
public static StateManager getInstance(Computer computer) {
if (instance == null) {
instance = new StateManager(computer);
}
return instance;
}
State alphaState;
Set<ObjectGraphNode> allStateVariables;
WeakHashMap<Object, ObjectGraphNode> objectLookup;
long maxMemory = -1;
long freeRequired = -1;
@ConfigurableField(category = "Emulator", name = "Max states", description = "How many states can be captured, oldest states are automatically truncated.", defaultValue = "150")
public int maxStates = 100;
@ConfigurableField(category = "Emulator", name = "Capture frequency", description = "How often states are captured, in relation to each VBL (1 = 60 states/second, 2 = 30 states/second, 3 = 15 states/second, etc", defaultValue = "3")
public int captureFrequency = 3;
private ObjectGraphNode<BufferedImage> imageGraphNode;
Computer computer;
private StateManager(Computer computer) {
this.computer = computer;
}
private void buildStateMap() {
allStateVariables = new LinkedHashSet<>();
objectLookup = new WeakHashMap<>();
ObjectGraphNode emulator = new ObjectGraphNode(Emulator.instance);
emulator.name = "Emulator";
Set visited = new HashSet();
buildStateMap(emulator, visited);
// Also track all softswitches
for (SoftSwitches s : SoftSwitches.values()) {
final SoftSwitches ss = s;
ObjectGraphNode switchNode = new ObjectGraphNode(s);
switchNode.name = s.toString();
ObjectGraphNode switchVar = new ObjectGraphNode(s.getSwitch()) {
@Override
/**
* This is a more efficient way of updating the softswitch
* states And really in a way this works out much better to
* ensure that memory and graphics pages are set up correctly
* when resuming states.
*/
public void setCurrentValue(Object value) {
Boolean b = (Boolean) value;
ss.getSwitch().setState(b);
}
@Override
public Object getCurrentValue() {
return ss.getSwitch().getState();
}
};
switchVar.name = "switch";
switchVar.parent = switchNode;
allStateVariables.add(switchVar);
objectLookup.put(s, switchNode);
objectLookup.put(s.getSwitch(), switchVar);
}
}
private void buildStateMap(ObjectGraphNode node, Set visited) {
if (visited.contains(node)) {
return;
}
Object currentValue = node.getCurrentValue();
if (currentValue == null || visited.contains(currentValue)) {
return;
}
visited.add(node);
visited.add(currentValue);
objectLookup.put(node.getCurrentValue(), node);
for (Field f : node.getCurrentValue().getClass().getFields()) {
try {
Object o = f.get(node.getCurrentValue());
if (o == null) {
continue;
}
Annotation a = f.getAnnotation(Stateful.class);
ObjectGraphNode child = new ObjectGraphNode(o);
child.name = f.getName();
child.parent = node;
if (a != null) {
child.isStateful = true;
addStateVariable(child, f);
}
if (!f.getType().isPrimitive() && !f.getType().isArray()) {
// This is not stateful, but examine its children just in case
buildStateMap(child, visited);
}
} catch (IllegalArgumentException | IllegalAccessException ex) {
Logger.getLogger(StateManager.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
/**
* The node and field correspond to an object member field that has a
*
* @Stateful annotation. This method has to make sense of this and plan out
* how states should be captured for this field.
* @param node
* @param f
*/
private void addStateVariable(ObjectGraphNode node, Field f) {
// if (f == null) {
// System.out.println("State var: "+node.parent.name+"."+node.name+ ">>" + node.index);
// } else {
// System.out.println("State var: "+node.parent.name+"."+node.name+ ">>" + f.getDeclaringClass().getName() + "." + f.getName());
// }
// For paged memory, video and softswiches we might have to do something
// more sophosticated.
Class type = node.getCurrentValue().getClass();
if (PagedMemory.class.isAssignableFrom(type)) {
addMemoryPages(node, f);
} else if (BufferedImage.class.isAssignableFrom(type)) {
addVideoFrame(node, f);
} else if (List.class.isAssignableFrom(type)) {
List l = (List) node.getCurrentValue();
Type fieldGenericType = f.getGenericType();
// Class genericType = Object.class;
// if (fieldGenericType instanceof ParameterizedType) {
// genericType = (Class) ((ParameterizedType) fieldGenericType).getActualTypeArguments()[0];
// } else {
// System.out.println("NOT PARAMATERIZED!");
// }
for (int i = 0; i < l.size(); i++) {
if (l.get(i) != null) {
// ObjectGraphNode inode = new ObjectGraphNode(genericType);
Object obj = l.get(i);
if (obj == null) {
continue;
}
ObjectGraphNode inode = new ObjectGraphNode(obj);
// inode.source = new WeakReference();
inode.parent = node;
inode.name = String.valueOf(i);
inode.index = i;
// Build this recursively because it might be a nested type (e.g. a list of maps)
// If it isn't a nested type then the default case shoud apply.
addStateVariable(inode, null);
}
}
} else if (Map.class.isAssignableFrom(type)) {
// TODO:
// Walk through members of the map, etc.
// This will at least let RamWorks memory pages work with state management
} else {
// This is the default case, just capture the field and move on
allStateVariables.add(node);
node.isPrimitive = f.getType().isPrimitive();
// Since we can only guess how state changes just assume we have to check for changes every time.
node.forceCheck = true;
}
}
/**
* Track a stateful video framebuffer.
*
* @param node
* @param f
*/
private void addVideoFrame(ObjectGraphNode<BufferedImage> node, Field f) {
imageGraphNode = node;
}
/**
* Track a stateful set of memory.
*
* @param node
* @param f
*/
private void addMemoryPages(ObjectGraphNode<PagedMemory> node, Field f) {
PagedMemory mem = node.getCurrentValue();
ObjectGraphNode<byte[][]> internalmem = new ObjectGraphNode<>(mem.internalMemory);
internalmem.parent = node;
internalmem.name = "internalMemory";
for (int i = 0; i < mem.internalMemory.length; i++) {
byte[] memPage = mem.internalMemory[i];
if (memPage == null) {
continue;
}
ObjectGraphNode<byte[]> page = new ObjectGraphNode<>(memPage);
page.parent = internalmem;
page.name = String.valueOf(i);
page.index = i;
page.forceCheck = false;
allStateVariables.add(page);
objectLookup.put(mem.internalMemory[i], page);
}
}
public static void markDirtyValue(Object o, Computer computer) {
StateManager manager = getInstance(computer);
if (manager.objectLookup == null) {
return;
}
ObjectGraphNode node = manager.objectLookup.get(o);
if (node == null) {
return;
}
node.markDirty();
}
/**
*
* @return
*/
@Override
public String getName() {
return "State Manager";
}
/**
*
* @return
*/
@Override
public String getShortName() {
return "state";
}
/**
* If reconfigure is called, it means the emulator state has changed too
* greatly and we need to abandon captured states and start from scratch.
*/
@Override
public void reconfigure() {
boolean resume = computer.pause();
isValid = false;
// Now figure out how much memory we're allowed to eat
maxMemory = Runtime.getRuntime().maxMemory();
// If we have less than 2% heap remaining then states will be recycled
freeRequired = maxMemory / 50L;
frameCounter = captureFrequency;
if (resume) {
computer.resume();
}
}
boolean isValid = false;
public void invalidate() {
isValid = false;
}
int stateCount = 0;
public void captureState() {
// If the state graph is invalidated it means we have to abandon all
// previously captured states. This helps ensure that rewinding will
// not result in an unintended or invalid state.
if (!isValid) {
alphaState = null;
if (allStateVariables != null) {
allStateVariables.clear();
}
allStateVariables = null;
// This will probably result in a lot of invalidated objects
// so it's a good idea to suggest to the JVM to reclaim memory now.
System.gc();
if (Emulator.instance == null) {
return;
}
// Re-examine the object structure of the emulator in case it changed
buildStateMap();
System.out.println(allStateVariables.size() + " variables tracked per state");
System.out.println(objectLookup.entrySet().size() + " objects tracked in emulator model");
isValid = true;
stateCount = 0;
}
if (alphaState == null) {
System.gc();
alphaState = captureAlphaState();
alphaState.tail = alphaState;
} else {
if (Runtime.getRuntime().freeMemory() <= freeRequired) {
invalidate();
return;
}
while (stateCount >= maxStates) {
removeOldestState();
stateCount--;
}
State newState = captureDeltaState(alphaState.tail);
// State newState = (stateCount % 2 == 0) ? captureDeltaState(alphaState.tail) : captureAlphaState();
alphaState.tail.addState(newState);
alphaState.tail = newState;
}
// Now capture the current screen
alphaState.tail.screenshot = getScreenshot();
stateCount++;
}
private Image getScreenshot() {
Image screen = computer.getVideo().getFrameBuffer();
return new WritableImage(screen.getPixelReader(), (int) screen.getWidth(), (int) screen.getHeight());
}
private State captureAlphaState() {
State s = new State();
s.deltaState = false;
allStateVariables.stream().map((node) -> {
s.put(node, new StateValue(node));
return node;
}).forEach((node) -> {
node.markClean();
});
return s;
}
private State captureDeltaState(State tail) {
State s = new State();
s.deltaState = true;
allStateVariables.stream().filter((node) -> !(!node.valueChanged(tail))).map((node) -> {
// If there are no changes to this node value, don't waste memory on it.
s.put(node, new StateValue(node));
return node;
}).forEach(ObjectGraphNode::markClean);
return s;
}
private void removeOldestState() {
if (alphaState == null) {
return;
}
if (alphaState.nextState == null) {
alphaState = null;
} else {
alphaState = alphaState.deleteNext();
}
}
// Don't capture for the first few seconds of emulation. This is sort of a
// hack but is also a very elegant way to help the emulator avoid wasting
// time at start since the emulator state changes a lot at first.
int frameCounter = 200;
/**
* Every time the Apple reaches VBL there is a screen update event The rest
* of the emulator is notified after the video frame was redrawn, so this is
* the best time to capture the state.
*/
public void notifyVBLActive() {
frameCounter--;
if (frameCounter > 0) {
return;
}
frameCounter = captureFrequency;
captureState();
}
@InvokableAction(
name = "Rewind",
alternatives = "Timewarp",
description = "Go back 1 second",
defaultKeyMapping = {"ctrl+shift+Open Bracket"}
)
public static void beKindRewind() {
StateManager manager = Emulator.withComputer(StateManager::getInstance, null);
new Thread(()->manager.rewind(60 / manager.captureFrequency)).start();
}
public void rewind(int numStates) {
boolean resume = computer.pause();
State state = alphaState.tail;
while (numStates > 0 && state.previousState != null) {
state = state.previousState;
numStates--;
}
state.apply();
alphaState.tail = state;
state.nextState = null;
Video.forceRefresh();
System.gc();
if (resume) {
computer.resume();
}
}
}