openai/retro

Can't load multiple games per emulator

onaclov2000 opened this issue · 5 comments

Issue summary

I wanted to understand the reasoning the code in retro.cpp checks if there is a rom loaded already and kills the system.

This line below checks if a rom is loaded, if so it'll unload it and load the "rom_path" provided
https://github.com/openai/retro/blob/master/src/emulator.cpp#L76

I just wanted to understand the reasoning for it.

I'm experimenting with loading different roms through a single emulation instance. Right now I have to close the emulator (env.close()), and open a new one (retro.make()), which means I get a new window, I'd like to load roms back to back via the same window, and think this is likely the answer.

I haven't verified everything but making these changes I was able to load roms back to back.

The patch is listed below.

This is how I used it;

# First instantiate the game
env = retro.make(game='SuperMarioWorld-Snes')
env.reset()
#...
# do some stuff
# finally load the rom
env.loadRom("DonkeyKongCountry-Snes")

Now I havent verified everything loads correctly, it works for my uses. I'm basically "live swapping" between rom hacks, so something like SMW above and SMW Kaizo would probably fit if we didn't correctly load the memory locations for variables/etc.

diff --git a/retro/retro_env.py b/retro/retro_env.py
index a5de20a..5d28677 100644
--- a/retro/retro_env.py
+++ b/retro/retro_env.py
@@ -84,6 +84,8 @@ class RetroEnv(gym.Env):
         # We can't have more than one emulator per process. Before creating an
         # emulator, ensure that unused ones are garbage-collected
         gc.collect()
+        print ("GARBAGE COLLECT?")
+        # Ideally we would create one retro emulator instance and then we could just load roms, but I haven't sorted that out yet
         self.em = retro.RetroEmulator(rom_path)
         self.em.configure_data(self.data)
         self.em.step()
@@ -136,6 +138,37 @@ class RetroEnv(gym.Env):
             self._render = self.render
             self._close = self.close
 
+    def loadRom(self, game):
+        # This successfully loads a rom, however it doesn't jump to the "start" state as defined in the json file.
+        metadata = {}
+        rom = retro.data.get_romfile_path(game, retro.data.Integrations.STABLE)
+        metadata_path = retro.data.get_file_path(game, 'metadata.json', retro.data.Integrations.STABLE)
+
+        try:
+            with open(metadata_path) as f:
+                metadata = json.load(f)
+            if 'default_player_state' in metadata and self.players <= len(metadata['default_player_state']):
+                self.statename = metadata['default_player_state'][self.players - 1]
+            elif 'default_state' in metadata:
+                self.statename = metadata['default_state']
+            else:
+                self.statename = None
+        except (IOError, json.JSONDecodeError):
+            pass
+
+        self.gamename = game
+        if self.statename:
+            self.load_state(self.statename, retro.data.Integrations.STABLE)
+        
+        self.em.loadRom(rom)
+
+        #self.em.configure_data(self.data)
+        #self.em.step()
+
+        self.reset()
+        obs, rew, done, info = self.step([0,0,0,0,0,0,0,0,0,0,0,0])
+
+
     def _update_obs(self):
         if self._obs_type == retro.Observations.RAM:
             self.ram = self.get_ram()
@@ -193,6 +226,7 @@ class RetroEnv(gym.Env):
     def reset(self):
         if self.initial_state:
             self.em.set_state(self.initial_state)
+
         for p in range(self.players):
             self.em.set_button_mask(np.zeros([self.num_buttons], np.uint8), p)
         self.em.step()
diff --git a/setup.py b/setup.py
index fab566c..c6c2b91 100644
--- a/setup.py
+++ b/setup.py
@@ -11,25 +11,11 @@ VERSION_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'VERSION
 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
 README = open(os.path.join(SCRIPT_DIR, "README.md")).read()
 
-if not os.path.exists(os.path.join(os.path.dirname(__file__), '.git')):
-    use_scm_version = False
-    shutil.copy('VERSION', 'retro/VERSION.txt')
-else:
-    def version_scheme(version):
-        with open(VERSION_PATH) as v:
-            version_file = v.read().strip()
-        if version.distance:
-            version_file += '.dev%d' % version.distance
-        return version_file
 
-    def local_scheme(version):
-        v = ''
-        if version.distance:
-            v = '+' + version.node
-        return v
-    use_scm_version = {'write_to': 'retro/VERSION.txt',
-                       'version_scheme': version_scheme,
-                       'local_scheme': local_scheme}
+#if not os.path.exists(os.path.join(os.path.dirname(__file__), '.git')):
+use_scm_version = False
+shutil.copy('VERSION', 'retro/VERSION.txt')
+
 
 
 class CMakeBuild(build_ext):
diff --git a/src/emulator.cpp b/src/emulator.cpp
index a74c279..e822913 100644
--- a/src/emulator.cpp
+++ b/src/emulator.cpp
@@ -58,6 +58,7 @@ Emulator::Emulator() {
 }
 
 Emulator::~Emulator() {
+        cout << "Destructor Called\n";
 	if (m_corePath) {
 		free(m_corePath);
 	}
@@ -82,10 +83,14 @@ bool Emulator::loadRom(const string& romPath) {
 	if (core.size() == 0) {
 		return false;
 	}
-
+        cout << core << "\n";
+        cout << m_coreHandle << "\n";
 	if (m_coreHandle && m_core != core) {
+                cout << "Unloading Core\n";
 		unloadCore();
 	}
+       cout << m_coreHandle << "\n";
+
 	if (!m_coreHandle) {
 		string lib = libForCore(core) + "_libretro.";
 #ifdef __APPLE__
@@ -99,16 +104,20 @@ bool Emulator::loadRom(const string& romPath) {
 			return false;
 		}
 		m_core = core;
+		
 	}
-
+        cout << m_coreHandle << "\n";
 	retro_game_info gameInfo;
 	ifstream in(romPath, ios::binary | ios::ate);
 	if (in.fail()) {
+		cout << "Game Info\n";
 		return false;
 	}
+
 	ostringstream out;
 	gameInfo.size = in.tellg();
 	if (in.fail()) {
+		cout << "Game Size\n";
 		return false;
 	}
 	char* romData = new char[gameInfo.size];
@@ -118,6 +127,7 @@ bool Emulator::loadRom(const string& romPath) {
 	in.read(romData, gameInfo.size);
 	if (in.fail()) {
 		delete[] romData;
+		cout << "IN failed\n";
 		return false;
 	}
 	in.close();
@@ -125,6 +135,7 @@ bool Emulator::loadRom(const string& romPath) {
 	auto res = retro_load_game(&gameInfo);
 	delete[] romData;
 	if (!res) {
+		cout << "Load Game Failed\n";
 		return false;
 	}
 	retro_get_system_av_info(&m_avInfo);
diff --git a/src/retro.cpp b/src/retro.cpp
index c631638..54ff913 100644
--- a/src/retro.cpp
+++ b/src/retro.cpp
@@ -35,15 +35,14 @@ struct PyRetroEmulator {
 	Retro::Emulator m_re;
 	int m_cheats = 0;
 	PyRetroEmulator(const string& rom_path) {
-		if (Emulator::isLoaded()) {
-			throw std::runtime_error("Cannot create multiple emulator instances per process, make sure to call env.close() on each environment before creating a new one");
-		}
 		if (!m_re.loadRom(rom_path.c_str())) {
 			throw std::runtime_error("Could not load ROM");
 		}
 		m_re.run(); // otherwise you get a segfault when you try to get screen for the first time
 	}
-
+	void loadRom(const string& rom_path){
+		m_re.loadRom(rom_path.c_str());
+	}
 	void step() {
 		m_re.run();
 	}
@@ -86,6 +85,7 @@ struct PyRetroEmulator {
 		return arr;
 	}
 
+        
 	double getAudioRate() {
 		return m_re.getAudioRate();
 	}
@@ -467,6 +467,7 @@ PYBIND11_MODULE(_retro, m) {
 		.def("step", &PyRetroEmulator::step)
 		.def("set_button_mask", &PyRetroEmulator::setButtonMask, py::arg("mask"), py::arg("player") = 0)
 		.def("get_state", &PyRetroEmulator::getState)
+    	.def("loadRom", &PyRetroEmulator::loadRom)
 		.def("set_state", &PyRetroEmulator::setState)
 		.def("get_screen", &PyRetroEmulator::getScreen)
 		.def("get_screen_rate", &PyRetroEmulator::getScreenRate)

Generally speaking, the Emulator class represents a single running game, and running games use one cartridge. With the exception of lock-on cartridges (e.g. Sonic & Knuckles, or Game Genies), cartridge swapping isn't...generally a thing in consoles. There's no guarantee that all of the cores used will handle it sanely, so the only sane widely-applicable thing to do is spin down the existing instance and spin up a new one.

Agreed, I was trying to "retrofit" this repo for another purpose. The main issue I was running into was if I closed the console and re-opened, it creates a new window, which I didn't want. If that wasn't an issue then closing the emulator loading the next one and re-loading a save state is a completely reasonable approach.

I've only confirmed it works with SNES (Super Metroid and it's various rom hacks) thus far.

I am going to close this, but mostly so if someone else had an interest in similar things they could find this post.

This is the version I made with the ability to load roms, I played it with 3 rom hacks and it worked great (for my purposes)

https://github.com/TwitchAIRacingLeague/retro/tree/HackRandomizer