Adding Save States to an Emulator
Learn how to add save states to your emulator by using utilizing software design patterns so you'll have infinite chances to catch that shiny Pikachu.
Table of Contents
Synopsis
You walk into that fated patch of grass then a shiny Pikachu suddenly appears. But uh-oh, you only have five PokéBalls 😔. If only you had a way to save this moment in time!
Objectives
Gain the ability to save and restore emulator states by implementing save states. Understand the memento design pattern and how to implement it. Know the difference between shallow and deep copying. Lastly, learn how to serialize and deserialize objects.
Terminology
Component
The definition of a component in this article means a part of the emulator that contains states like the CPU, GPU, memory bus, ..etc.
Save State
A save state is an object that contains an emulator's exact state the moment it was saved. It contains properties like the registers, pc, joypad state, or memory contents.
I have a question for my computer scientists/software engineers. Which design pattern best fits the description of a save state? If you answered the memento design pattern, you're right!
Memento Design Pattern
Memento is a behavioral design pattern that allows the saving and restoration of an object's state without revealing the internal details of its implementation.
For us, each component is an originator that contains a nested class to represent its memento/snapshot. The caretaker represents the emulator that controls the creation and restoration of snapshots.
Shallow Copying
Shallow copying means copying an object reference, and the cloned object points to the original object.
Imagine you have a backend service with Customers
and periodically create customer backups.
Customer.java1public class Customer { 2 int[] orderNumbers; 3 String name; 4 int age; 5 6 // Shallow copying 7 public Customer(int[] orderNumbers, String name, int age) { 8 this.orderNumbers = orderNumbers; 9 this.name = name; 10 this.age = age; 11 } 12}
Create a customer backup1// Creating a customer 2int[] orders = [12, 3, 1]; 3String name = "Naruto Uzamaki"; 4int age = 16; 5 6Customer customer = new Customer(orders, name, age); 7 8// Creating a customer backup 9Customer customerBackup = new Customer(customer.orderNumbers, customer.name, 10 customer.age);
Shallow copies have a fatal flaw since each object shares the same reference with the original; if the original updates, the copy updates.
Primitive data types aren't object references, so they are unaffected.
1// Primitive data type are unnafected by changes in the original 2System.out.println(customer.name); // Naruto Uzamaki 3System.out.println(backupCusomer.name); // Naruto Uzamaki 4 5customer.updateName("Sasuke Uchiha"); 6 7System.out.println(customer.name); // Sasuke Uchiha 8System.out.println(backupCustomer.name); // Naruto Uzamaki 9 10// Object are affected by changes in the original 11System.out.println(Arrays.toString(customer.orders)); // [12, 3, 1] 12System.out.println(Arrays.toString(backupCusomer.orders)); // [12, 3, 1] 13 14customer.order[2] = 0xDEADBEEF 15 16System.out.println(Arrays.toString(customer.orders)); // [12, 3, 0xDEADBEEF] 17System.out.println(Arrays.toString(backupCusomer.orders)); // [12, 3, 0xDEADBEEF]
This technique isn't viable when making independent copies (such as snapshots of components). Luckily, deep copying comes to the rescue.
Deep Copying / Defensive Copying
Deep copying or defensive copying creates an independent copy by copying the object itself rather than passing a reference. This way changes in the original don't affect the copy.
Below is a modification of the Customer
class with deep copying.
Customer with deep copying1public class Customer { 2 int[] orderNumbers; 3 String name; 4 int age; 5 6 public Customer(int[] orderNumbers, String name, int age) { 7 // Deep copy the order numbers array object 8 this.orderNumbers = Arrays.copyOf(orderNumbers, orderNumbers.length); 9 10 // Primitive data types aren't 11 // object references. 12 this.name = name; 13 this.age = age; 14 } 15}
Getters can also utilize deep copying to return a defensive copy to prevent the caller from modifying the underlying object.
1// Return a defensive copy of the order numbers object. 2public int[] getOrderNumbers() { 3 return Arrays.copyOf(orderNumbers, orderNumbers.length); 4}
Serializable
Serializable
is a Java interface that enables classes to be represented as a sequence of bytes or serializable. This sequence can be sent over the network or saved to a file. Each object field in a serialized class must also implement Serializable
.
Classes that implement Serializable
require a serialVersionUID
variable to detect compatibility changes when deserializing. The serialVersionUID
must increment on each incompatible class change. For example, removing a class field makes the class incompatible with previously serialized versions.
Now we can save Customer
backups to a file.
1// Implement Serializable 2class Customer implements Serializable { 3 long serialVersionUID = 0 4 ... 5} 6 7// Save the customer to a file. 8public void saveCustomerToFile(Customer customer, String fileName) { 9 ObjectOutputStream objectOutputStream = new ObjectOutputStream( 10 new FileOutputStream(fileName)); 11 objectOutputStream.writeObject(customer); 12 objectOutputStream.close(); 13}
Explore the Mock Emulator
Below is a mock emulator with similar properties to a real emulator.
CPU.java1public class CPU { 2 private MemoryBus memoryBus; 3 private static final int PIPELINE_LENGTH = 3; 4 5 // 3 Stage pipeline 6 private int[] pipeline = new int[PIPELINE_LENGTH]; 7 8 // General Purpose Registers 9 private int[] registers = new int[15]; 10 ... 11}
Joypad.java1public class Joypad { 2 private boolean isButtonAPressed; 3 private boolean isButtonBPressed; 4 private boolean isSelectPressed; 5 private boolean isStartPressed; 6 private boolean isRightPressed; 7 private boolean isLeftPressed; 8 private boolean isUpPressed; 9 private boolean isDownPressed; 10 private boolean isButtonRPressed; 11 private boolean isButtonLPressed; 12 ... 13}
LCD.java1public class LCD { 2 private MemoryBus memoryBus; 3 4 private static final int SCREEN_WIDTH = 240; 5 private static final int SCREEN_HEIGHT = 160; 6 private static final int PIXEL_DEPTH = 4; 7 8 private int[] graphicsBuffer = 9 new int[SCREEN_WIDTH * SCREEN_HEIGHT * PIXEL_DEPTH]; 10 ... 11}
MemoryBus.java1public class MemoryBus { 2 private int[] ram = new int[0x1000]; 3 ... 4}
Save State / Snapshot Design
Studying the memento pattern, we learn that the originator creates snapshots that contain internal attributes. In our case, the originator represents a component.
We define an interface to expose methods for creating and restoring snapshots to share functionality. Each component has a unique snapshot class, so we introduce the generic type <T>
for representing the snapshot.
Snapshotable.java1public interface Snapshotable<T> { 2 T createSnapshot(); 3 boolean restoreSnapshot(T snapshot); 4}
Each component implements Snapshotable
and passes a nested snapshot class as the generic <T>
. The nested snapshot replaces the T
in the implemented Snapshotable
methods for that component.
1class CPUSnapshot { 2 private final int[] pipeline; 3 private final int[] registers; 4 5 // Deep copy attributes 6 CPUSnapshot(int[] pipeline, int[] registers) { 7 this.pipeline = Arrays.copyOf(pipeline, pipeline.length); 8 this.registers = Arrays.copyOf(registers, registers.length); 9 } 10 11 // Return a defensive copy 12 public int[] getPipeline() { 13 return Arrays.copyOf(pipeline, pipeline.length); 14 } 15 16 // Return a defensive copy 17 public int[] getRegisters() { 18 return Arrays.copyOf(registers, registers.length); 19 } 20} 21 22public class CPU implements Snapshotable<CPUSnapshot> { 23 ... 24 25 @Override 26 public CPUSnapshot createSnapshot() { 27 ... 28 } 29 30 @Override 31 public boolean restoreSnapshot(CPUSnapshot snapshot) { 32 ... 33 } 34}
To prevent issues with shallow copying the snapshot needs to be immutable. The constructor deep copies each attribute, and getters return deep copies to prevent callers from modifying the underlying object.
Since snapshots are nested classes, attributes are passed directly to the snapshots' constructor by the component rather than passing the component object itself, preventing the need to create getters for each attribute which could leak implementation details.
Creating the Component Snapshots
Now we create all the nested snapshot classes.
1class CPUSnapshot { 2 private final int[] pipeline; 3 private final int[] registers; 4 5 // Deep copy attributes 6 CPUSnapshot(int[] pipeline, int[] registers) { 7 this.pipeline = Arrays.copyOf(pipeline, pipeline.length); 8 this.registers = Arrays.copyOf(registers, registers.length); 9 } 10 11 // Return a defensive copy 12 public int[] getPipeline() { 13 return Arrays.copyOf(pipeline, pipeline.length); 14 } 15 16 // Return a defensive copy 17 public int[] getRegisters() { 18 return Arrays.copyOf(registers, registers.length); 19 } 20} 21 22public class CPU implements Snapshotable<CPUSnapshot> { 23 private MemoryBus memoryBus; 24 25 private static final int PIPELINE_LENGTH = 3; 26 27 // 3 Stage pipeline 28 private int[] pipeline = new int[PIPELINE_LENGTH]; 29 30 // General Purpose Registers 31 private int[] registers = new int[15]; 32 33 public CPU(MemoryBus memoryBus) { 34 this.memoryBus = memoryBus; 35 } 36 37 @Override 38 public CPUSnapshot createSnapshot() { 39 return new CPUSnapshot(pipeline, registers); 40 } 41 42 @Override 43 public boolean restoreSnapshot(CPUSnapshot snapshot) { 44 this.pipeline = snapshot.getPipeline(); 45 this.registers = snapshot.getRegisters(); 46 return true; 47 } 48}
1class JoypadSnapshot { 2 private final boolean isButtonAPressed; 3 private final boolean isButtonBPressed; 4 private final boolean isSelectPressed; 5 private final boolean isStartPressed; 6 private final boolean isRightPressed; 7 private final boolean isLeftPressed; 8 private final boolean isUpPressed; 9 private final boolean isDownPressed; 10 private final boolean isButtonRPressed; 11 private final boolean isButtonLPressed; 12 13 public JoypadSnapshot(boolean isButtonAPressed, boolean isButtonBPressed, 14 boolean isSelectPressed, boolean isStartPressed, boolean isRightPressed, 15 boolean isLeftPressed, boolean isUpPressed, boolean isDownPressed, 16 boolean isButtonRPressed, boolean isButtonLPressed) { 17 this.isButtonAPressed = isButtonAPressed; 18 this.isButtonBPressed = isButtonBPressed; 19 this.isSelectPressed = isSelectPressed; 20 this.isStartPressed = isStartPressed; 21 ... 22 } 23 24 ... 25} 26 27public class Joypad implements Snapshotable<JoypadSnapshot> { 28 private MemoryBus memoryBus; 29 30 private boolean isButtonAPressed; 31 private boolean isButtonBPressed; 32 private boolean isSelectPressed; 33 private boolean isStartPressed; 34 private boolean isRightPressed; 35 private boolean isLeftPressed; 36 private boolean isUpPressed; 37 private boolean isDownPressed; 38 private boolean isButtonRPressed; 39 private boolean isButtonLPressed; 40 41 public Joypad(MemoryBus memoryBus) { 42 this.memoryBus = memoryBus; 43 } 44 45 @Override 46 public JoypadSnapshot createSnapshot() { 47 return new JoypadSnapshot(isButtonAPressed, isButtonBPressed, 48 isSelectPressed, isStartPressed,isRightPressed, isLeftPressed, 49 isUpPressed, isDownPressed, isButtonRPressed, isButtonLPressed 50 ); 51 } 52 53 @Override 54 public boolean restoreSnapshot(JoypadSnapshot snapshot) { 55 isButtonAPressed = snapshot.isButtonAPressed(); 56 isButtonBPressed = snapshot.isButtonBPressed(); 57 isSelectPressed = snapshot.isSelectPressed(); 58 isStartPressed = snapshot.isStartPressed(); 59 isRightPressed = snapshot.isRightPressed(); 60 isLeftPressed = snapshot.isLeftPressed(); 61 isUpPressed = snapshot.isUpPressed(); 62 isDownPressed = snapshot.isDownPressed(); 63 isButtonRPressed = snapshot.isButtonRPressed(); 64 isButtonLPressed = snapshot.isButtonLPressed(); 65 return true; 66 } 67}
1class LCDSnapshot { 2 private final int[] graphicsBuffer; 3 4 public LCDSnapshot(int[] graphicsBuffer) { 5 this.graphicsBuffer = Arrays.copyOf(graphicsBuffer, graphicsBuffer.length); 6 } 7 8 public int[] getGraphicsBuffer() { 9 return Arrays.copyOf(graphicsBuffer, graphicsBuffer.length); 10 } 11} 12 13public class LCD implements Snapshotable<LCDSnapshot> { 14 private MemoryBus memoryBus; 15 16 private static final int SCREEN_WIDTH = 240; 17 private static final int SCREEN_HEIGHT = 160; 18 private static final int PIXEL_DEPTH = 4; 19 20 private int[] graphicsBuffer = 21 new int[SCREEN_WIDTH * SCREEN_HEIGHT * PIXEL_DEPTH]; 22 23 public LCD(MemoryBus memoryBus) { 24 this.memoryBus = memoryBus; 25 } 26 27 public int[] getGraphicsBuffer() { 28 return graphicsBuffer; 29 } 30 31 @Override 32 public LCDSnapshot createSnapshot() { 33 return new LCDSnapshot(graphicsBuffer); 34 } 35 36 @Override 37 public boolean restoreSnapshot(LCDSnapshot snapshot) { 38 this.graphicsBuffer = snapshot.getGraphicsBuffer(); 39 return true; 40 } 41}
1class MemoryBusSnapshot { 2 private final int[] ram; 3 4 public MemoryBusSnapshot(int[] ram) { 5 this.ram = Arrays.copyOf(ram, ram.length); 6 } 7 8 public int[] getRam() { 9 return Arrays.copyOf(ram, ram.length); 10 } 11} 12 13public class MemoryBus implements Snapshotable<MemoryBusSnapshot> { 14 private int[] ram = new int[0x1000]; 15 16 @Override 17 public MemoryBusSnapshot createSnapshot() { 18 return new MemoryBusSnapshot(ram); 19 } 20 21 @Override 22 public boolean restoreSnapshot(MemoryBusSnapshot snapshot) { 23 this.ram = snapshot.getRam(); 24 return true; 25 } 26}
Creating a Save State
Create a SaveState
class to contain all the snapshots and represent a save state. Next, collect all the snapshots then initialize a SaveState
object. This creates a perfect snapshot of the emulator's current state.
1public class SaveState { 2 private CPUSnapshot cpuSnapshot; 3 private JoypadSnapshot joypadSnapshot; 4 private LCDSnapshot lcdSnapshot; 5 private MemoryBusSnapshot memoryBusSnapshot; 6 7 public SaveState(CPUSnapshot cpuSnapshot, JoypadSnapshot joypadSnapshot, 8 LCDSnapshot lcdSnapshot, MemoryBusSnapshot memoryBusSnapshot) { 9 this.cpuSnapshot = cpuSnapshot; 10 this.joypadSnapshot = joypadSnapshot; 11 this.lcdSnapshot = lcdSnapshot; 12 this.memoryBusSnapshot = memoryBusSnapshot; 13 } 14 15 public CPUSnapshot getCPUSnapshot() { 16 return cpuSnapshot; 17 } 18 19 public JoypadSnapshot getJoypadSnapshot() { 20 return joypadSnapshot; 21 } 22 23 public LCDSnapshot getLCDSnapshot() { 24 return lcdSnapshot; 25 } 26 27 public MemoryBusSnapshot getMemoryBusSnapshot() { 28 return memoryBusSnapshot; 29 } 30}
Restoring a Save State
To restore a save state, restore each component using its corresponding snapshot contained in a SaveState
object to restore the emulator to the moment the save state was created.
1public void restoreSaveState(SaveState saveState) { 2 cpu.restoreSnapshot(saveState.getCPUSnapshot()); 3 joypad.restoreSnapshot(saveState.getJoypadSnapshot()); 4 lcd.restoreSnapshot(saveState.getLCDSnapshot()); 5 memoryBus.restoreSnapshot(saveState.getMemoryBusSnapshot()); 6} 7... 8SaveState saveState = 9 new SaveState(cpuSnapshot, joypadSnapshot, lcdSnapshot memoryBusSnapshot); 10 11restoreSaveState(saveState);
Serializing
To serialize a save state, implement Serializable
on the SaveState
and snapshot class(es) and add a serialVersionUID
to each to declare the class version. The SaveState
can not be saved to a file or sent over the network somewhere else, whatever you want to do.
1class SaveState implements Serializable { 2 private static final long serialVersionUID = 0; 3... 4class CPUSnapshot implements Serializable { 5 private static final long serialVersionUID = 0; 6... 7class MemoryBusSnapshot implements Serializable { 8 private static final long serialVersionUID = 0; 9... 10class LCDSnapshot implements Serializable { 11 private static final long serialVersionUID = 0; 12... 13class MemoryBusSnapshot implements Serializable { 14 private static final long serialVersionUID = 0;
Saving to a File
Saving a save state to a file is as simple as it sounds. Write the serialized SaveState
object to a file.
1public static void serializeSaveState(SaveState saveState, String fileName) { 2 try { 3 ObjectOutputStream objectOutputStream = new ObjectOutputStream( 4 new FileOutputStream(fileName)); 5 objectOutputStream.writeObject(saveState); 6 objectOutputStream.close(); 7 } catch (Exception ex) { 8 ex.printStackTrace(); 9 } 10}
Loading from a File
Again, just as easy as it sounds, read the SaveState
object bytes into a SaveState
object.
1public static SaveState deserializeSaveState(String fileName) { 2 ObjectInputStream saveStateBytes = new ObjectInputStream( 3 new BufferedInputStream(new FileInputStream( 4 fileName))); 5 6 return (SaveState) saveStateBytes.readObject(); 7}
Compressing with Gzip
In a full-fledged emulator, a save state can get mighty big. Let's add gzip
compression to the mix to save precious bytes.
1public static void serializeSaveState(SaveState saveState, String fileName) { 2 // Add GZIPOutputStream in-front of the FileOutputStream 3 ObjectOutputStream compressedObjectOutputStream = new ObjectOutputStream( 4 new GZIPOutputStream(new FileOutputStream(fileName))); 5 compressedObjectOutputStream.writeObject(saveState); 6 compressedObjectOutputStream.close(); 7} 8 9public static SaveState deserializeSaveState(String fileName) { 10 // Add GZIPInputStream in-front of the FileInputStream 11 ObjectInputStream decompressedSaveStateBytes = new ObjectInputStream( 12 new BufferedInputStream(new GZIPInputStream(new FileInputStream( 13 fileName)))); 14 15 return (SaveState) decompressedSaveStateBytes.readObject(); 16}
Conclusion
Save states are handy for instantly saving and restoring game progress. Now you're ready to catch your shiny Pikachu and disregard RNG entirely. Adding this feature is super fun and enlightening to see niche projects like emulators benefiting from traditional design patterns.
I hope you learned how to add save states to your emulators. Send any questions my way using my contacts.
Consider signing up for my newsletter or supporting me if you enjoyed the article.