Reverse Engineering

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.

Written by Gregory Gaines
8 min read
0 views
Floppy disks

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.

Java
Customer.java
1public 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}
Java
Create a customer backup
1// 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.

Java
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.

Java
Customer with deep copying
1public 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.

Java
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.

Java
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.

Java
CPU.java
1public 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}
Java
Joypad.java
1public 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}
Java
LCD.java
1public 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}
Java
MemoryBus.java
1public 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.

Java
Snapshotable.java
1public 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.

Java
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.

Java
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}
Java
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}
Java
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}
Java
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.

Java
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.

Java
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.

Java
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.

Java
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.

Java
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.

Java
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.

Resources

About the author.

I'm Gregory Gaines, a software engineer that loves blogging, studying computer science, and reverse engineering.

I'm currently employed at Google; all opinions are my own.

Ko-fi donationsBuy Me a CoffeeBecome a Patron
Gregory Gaines

You may also like.

Comments.

Get updates straight to your mailbox!

Get the latest blog updates about programming and the industry ins and outs for free!

You have my spam free, guarantee. 🥳

Subscribe