cynes - C/C++ NES emulator with Python bindings
cynes is a lightweight multiplatform NES emulator providing a simple Python interface. The core of the emulation is based on the very complete documentation provided by the Nesdev Wiki. The current implementation consists of
- A cycle-accurate CPU emulation
- A cycle-accurate PPU emulation
- A cycle-accurate APU emulation (even though it does not produce any sound)
- Few basic NES mappers (more to come)
The Python bindings allow to interact easily with one or several NES emulators at the same time, ideal for machine learning application.
Installation
cynes can be installed using pip :
pip install cynes
It can also be built from source using (requires cmake
) :
python setup.py build
How to use
A cynes NES emulator can be created by instanticiating a new NES object. The following code is the minimal code to run a ROM file.
from cynes.windowed import WindowedNES
with WindowedNES("rom.nes") as nes:
while not nes.should_close:
frame = nes.step()
Multiple emulators can be created at once by instantiating several NES objects.
Windowed / Headless modes
The default NES class run in "headless" mode, meaning that no rendering is performed. A simple wrapper around the base emulator providing a basic renderer and input handling using SDL2 is present in the windowed
submodule.
from cynes import NES
from cynes.windowed import WindowedNES
nes_headless = NES("rom.nes")
while not nes_headless.has_crashed:
frame = nes_headless.step()
nes_windowed = WindowedNES("rom.nes")
while not nes_windowed.should_close:
frame = nes_windowed.step()
While the rendering overhead is quite small, running in headless mode can improve the performances when the window is not needed. The content of the frame buffer can always be accessed using the step
method.
Controller
The state of the controller can be directly modified using the following syntax :
from cynes import *
nes.controller = NES_INPUT_RIGHT
nes.controller = NES_INPUT_RIGHT | NES_INPUT_A
nes.controller = NES_INPUT_START
nes.controller |= NES_INPUT_B
nes.controller |= NES_INPUT_SELECT
nes.controller = NES_INPUT_RIGHT | NES_INPUT_LEFT
nes.controller = NES_INPUT_DOWN | NES_INPUT_UP
nes.step(frames=5)
Note that the state of the controller is maintained even after the step
method is called. This means that it has to be reset to 0 to release the buttons.
Two controllers can be used at the same time. The state of the second controller can be modified by updating the 8 most significant bits of the same variable.
nes.controller = NES_INPUT_LEFT | NES_INPUT_RIGHT << 8
Key handlers
Key handlers are a simple way of associating custom actions to shortcuts. This feature is only present with the windowed mode. The key events (and their associated handlers) are fired when calling the step
method.
nes = WindowedNES("rom.nes", default_handlers=False)
import sdl2
def kill():
nes.close()
nes.register(sdl2.SDL_SCANCODE_O, kill)
By default, the emulator comes with key handlers that map window keys to the controller buttons. The mapping is the following :
- the arrow keys for the D-pad
- the keys X and Z for the A and B buttons respectively
- the keys A and S for the SELECT and START buttons respectively
Save states
The state of the emulator can be saved as a numpy array and later be restored.
save_state = nes.save()
nes.load(save_state)
Memory modification should never be performed directly on a save state, as it is prone to memory corruption. Theses two methods can be quite slow, therefore, they should be called sparsely.
Memory access
The memory of the emulator can be read from and written to using the following syntax :
player_state = nes[0x000E]
nes[0x075A] = 0x8
Note that only the CPU RAM $0000 - $1FFFF
and the mapper RAM $6000 - $7FFF
should be accessed. Trying to read / write a value to other addresses may desynchronize the components of the emulator, resulting in a undefined behavior.
Closing
An emulator is automatically closed when the object is released by Python. In windowed mode, the close
method can be used to close the window without having to wait for Python to release the object. As presented previously, the WindowedNES can also be used as a context manager, which will call close
automatcially when exiting the context.
It can also be closed manualy using the close
method.
nes.close()
del nes
nes.close()
nes.should_close
When the emulator is closed, but the object is not deleted yet, the should_close
property will be set to True, indicating that calling any NES function will not work properly. This method can also return True in two other cases :
- When the CPU of the emulator is frozen. When the CPU hits a JAM instruction (illegal opcode), it is frozen until the emulator is reset. This should never happen, but memory corruptions can cause them, so be careful when accessing the NES memory.
- In windowed mode, when the window is closed or when the ESC key is pressed.
License
This project is licensed under GPL-3.0
cynes - C/C++ NES emulator with Python bindings
Copyright (C) 2021 - 2024 Combey Theo
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.