Skip to content
Snippets Groups Projects
retro.cpp 9.45 KiB
Newer Older
  • Learn to ignore specific revisions
  • #include <cmath>
    
    #ifdef _WIN32
    // pyconfig.h doesn't seem to like hypot, so we need to work around it.
    namespace std {
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    template<typename T>
    static inline T _hypot(T x, T y) {
    	return hypot(x, y);
    }
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    #include <pybind11/pybind11.h>
    #include <pybind11/numpy.h>
    
    #include "coreinfo.h"
    #include "data.h"
    #include "emulator.h"
    #include "imageops.h"
    #include "memory.h"
    #include "script.h"
    #include "movie.h"
    #include "movie-bk2.h"
    
    #include <map>
    #include <unordered_map>
    #include <unordered_set>
    
    namespace py = pybind11;
    
    using std::string;
    using namespace Retro;
    
    struct PyGameData;
    struct PyRetroEmulator {
    	Retro::Emulator m_re;
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	int m_cheats = 0;
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	PyRetroEmulator(const string& rom_path) {
    		if (Emulator::isLoaded()) {
    			throw std::runtime_error("Cannot create multiple emulator instances per process");
    		}
    		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 step() {
    		m_re.run();
    	}
    
    	py::bytes getState() {
    		size_t size = m_re.serializeSize();
    		py::bytes bytes(NULL, size);
    		m_re.serialize(PyBytes_AsString(bytes.ptr()), size);
    		return bytes;
    	}
    
    	bool setState(py::bytes o) {
    		return m_re.unserialize(PyBytes_AsString(o.ptr()), PyBytes_Size(o.ptr()));
    	}
    
    	py::array_t<uint8_t> getScreen() {
    		long w = m_re.getImageWidth();
    		long h = m_re.getImageHeight();
    		py::array_t<uint8_t> arr({ { h, w, 3 } });
    		uint8_t* data = arr.mutable_data();
    		Image out(Image::Format::RGB888, data, w, h, w);
    		Image in;
    		if (m_re.getImageDepth() == 16) {
    			in = Image(Image::Format::RGB565, m_re.getImageData(), w, h, m_re.getImagePitch());
    		} else if (m_re.getImageDepth() == 32) {
    			in = Image(Image::Format::RGBX888, m_re.getImageData(), w, h, m_re.getImagePitch());
    		}
    		in.copyTo(&out);
    		return arr;
    	}
    
    	double getScreenRate() {
    		return m_re.getFrameRate();
    	}
    
    	py::array_t<int16_t> getAudio() {
    		py::array_t<int16_t> arr(py::array::ShapeContainer{ m_re.getAudioSamples(), 2 });
    		int16_t* data = arr.mutable_data();
    		memcpy(data, m_re.getAudioData(), m_re.getAudioSamples() * 4);
    		return arr;
    	}
    
    	double getAudioRate() {
    		return m_re.getAudioRate();
    	}
    
    	py::tuple getResolution() {
    		return py::make_tuple(m_re.getImageWidth(), m_re.getImageHeight());
    	}
    
    	void setButtonMask(py::array_t<uint8_t> mask, int player) {
    		if (mask.size() > N_BUTTONS) {
    			throw std::runtime_error("mask.size() > N_BUTTONS");
    		}
    		for (int key = 0; key < mask.size(); ++key) {
    			m_re.setKey(player, key, mask.data()[key]);
    		}
    	}
    
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	void addCheat(const string& code) {
    		m_re.setCheat(m_cheats, true, code.c_str());
    		++m_cheats;
    	}
    
    	void clearCheats() {
    		m_re.clearCheats();
    		m_cheats = 0;
    	}
    
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	void configureData(PyGameData& data);
    	static bool loadCoreInfo(const string& json) {
    		return Retro::loadCoreInfo(json);
    	}
    };
    
    struct PyMemoryView {
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	Retro::AddressSpace& m_mem;
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	PyMemoryView(Retro::AddressSpace& mem)
    		: m_mem(mem) {
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	}
    
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	int64_t extract(size_t address, const string& type) {
    		return m_mem[Variable{ type, address }];
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	}
    
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	void assign(size_t address, const string& type, int64_t value) {
    		m_mem[Variable{ type, address }] = value;
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	}
    
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	void setitem(py::dict item, int64_t value) {
    		return assign(py::int_(item["address"]), py::str(item["type"]), value);
    	}
    
    	int64_t getitem(py::dict item) {
    		return extract(py::int_(item["address"]), py::str(item["type"]));
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    	}
    };
    
    struct PyGameData {
    	Retro::GameData m_data;
    	Retro::Scenario m_scen{ m_data };
    
    	bool load(py::handle data = py::none(), py::handle scen = py::none()) {
    		ScriptContext::reset();
    
    		bool success = true;
    		if (!data.is_none()) {
    			success = success && m_data.load(py::str(data));
    		}
    		if (!scen.is_none()) {
    			success = success && m_scen.load(py::str(scen));
    		}
    		return success;
    	}
    
    	bool save(py::handle data = py::none(), py::handle scen = py::none()) {
    		bool success = true;
    		if (!data.is_none()) {
    			success = success && m_data.save(py::str(data));
    		}
    		if (!scen.is_none()) {
    			success = success && m_scen.save(py::str(scen));
    		}
    		return success;
    	}
    
    	void reset() {
    		m_scen.reloadScripts();
    	}
    
    	uint16_t filterAction(uint16_t action) const {
    		return m_scen.filterAction(action);
    	}
    
    	py::list validActions() const {
    		py::list outer;
    		for (const auto& action : m_scen.validActions()) {
    			py::list inner;
    			for (const auto& act : action.second) {
    				inner.append(act);
    			}
    			outer.append(inner);
    		}
    		return outer;
    	}
    
    	void updateRam() {
    		m_data.updateRam();
    	}
    
    	py::dict lookupAll() const {
    		py::dict data;
    		for (const auto& var : m_data.lookupAll()) {
    			data[py::str(var.first)] = var.second;
    		}
    		return data;
    	}
    
    	py::dict getVariable(py::str name) const {
    		py::dict obj;
    		Retro::Variable var = m_data.getVariable(name);
    		obj["address"] = var.address;
    		obj["type"] = var.type.type;
    		return obj;
    	}
    
    	void setVariable(py::str name, py::dict obj) {
    		Retro::Variable var{ string(py::str(obj["type"])), py::int_(obj["address"]) };
    		m_data.setVariable(name, var);
    	}
    
    	void removeVariable(py::str name) {
    		m_data.removeVariable(name);
    	}
    
    	py::dict listVariables() {
    		const auto& vars = m_data.listVariables();
    		py::dict vdict;
    		for (const auto& var : vars) {
    			const auto& v = var.second;
    			vdict[py::str(var.first)] = py::dict(py::arg("address") = v.address, py::arg("type") = v.type.type);
    		}
    		return vdict;
    	}
    
    	float currentReward() const {
    		return m_scen.currentReward();
    	}
    
    	bool isDone() const {
    		return m_scen.isDone();
    	}
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    
    	PyMemoryView memory() {
    		return PyMemoryView(m_data.addressSpace());
    	}
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    };
    
    void PyRetroEmulator::configureData(PyGameData& data) {
    	m_re.configureData(&data.m_data);
    }
    
    struct PyMovie {
    	std::unique_ptr<Retro::Movie> m_movie;
    	bool recording = false;
    	PyMovie(py::str name, bool record) {
    		recording = record;
    		if (record) {
    			m_movie = std::make_unique<MovieBK2>(name, true);
    		} else {
    			m_movie = Movie::load(name);
    		}
    		if (!m_movie) {
    			throw std::runtime_error("Could not load movie");
    		}
    	}
    
    	void configure(py::str name, const PyRetroEmulator& emu) {
    		if (recording) {
    			static_cast<MovieBK2*>(m_movie.get())->setGameName(name);
    			static_cast<MovieBK2*>(m_movie.get())->loadKeymap(emu.m_re.core());
    		}
    	}
    
    	py::str getGameName() const {
    		return m_movie->getGameName();
    	}
    
    	bool step() {
    		return m_movie->step();
    	}
    
    	void close() {
    		m_movie->close();
    	}
    
    	bool getKey(int key) {
    		return m_movie->getKey(key);
    	}
    
    	void setKey(int key, bool set) {
    		return m_movie->setKey(key, set);
    	}
    
    	py::bytes getState() {
    		std::vector<uint8_t> data;
    		m_movie->getState(&data);
    		return py::bytes(reinterpret_cast<const char*>(data.data()), data.size());
    	}
    
    	void setState(py::bytes data) {
    		m_movie->setState(reinterpret_cast<uint8_t*>(PyBytes_AsString(data.ptr())), PyBytes_Size(data.ptr()));
    	}
    };
    
    py::str corePath(py::handle hint = py::none()) {
    	return Retro::corePath(py::str(hint));
    }
    
    py::str dataPath(py::handle hint = py::none()) {
    	return Retro::GameData::dataPath(py::str(hint));
    }
    
    PYBIND11_MODULE(_retro, m) {
    	m.doc() = "libretro bindings";
    
    	py::class_<PyRetroEmulator>(m, "RetroEmulator")
    		.def(py::init<const string&>())
    		.def("step", &PyRetroEmulator::step)
    		.def("set_button_mask", &PyRetroEmulator::setButtonMask, py::arg("mask"), py::arg("player") = 0)
    		.def("get_state", &PyRetroEmulator::getState)
    		.def("set_state", &PyRetroEmulator::setState)
    		.def("get_screen", &PyRetroEmulator::getScreen)
    		.def("get_screen_rate", &PyRetroEmulator::getScreenRate)
    		.def("get_audio", &PyRetroEmulator::getAudio)
    		.def("get_audio_rate", &PyRetroEmulator::getAudioRate)
    		.def("get_resolution", &PyRetroEmulator::getResolution)
    		.def("configure_data", &PyRetroEmulator::configureData)
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    		.def("add_cheat", &PyRetroEmulator::addCheat)
    		.def("clear_cheats", &PyRetroEmulator::clearCheats)
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    		.def_static("load_core_info", &PyRetroEmulator::loadCoreInfo);
    
    	py::class_<PyMemoryView>(m, "Memory")
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    		.def(py::init<Retro::AddressSpace&>())
    		.def("extract", &PyMemoryView::extract, py::arg("address"), py::arg("type"))
    		.def("assign", &PyMemoryView::assign, py::arg("address"), py::arg("type"), py::arg("value"))
    		.def("__setitem__", &PyMemoryView::setitem, py::arg("item"), py::arg("value"))
    		.def("__getitem__", &PyMemoryView::getitem, py::arg("item"));
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    
    	py::class_<PyGameData>(m, "GameDataGlue")
    		.def(py::init<>())
    		.def("load", &PyGameData::load, py::arg("data") = py::none(), py::arg("scen") = py::none())
    		.def("save", &PyGameData::save, py::arg("data") = py::none(), py::arg("scen") = py::none())
    		.def("reset", &PyGameData::reset)
    		.def("filter_action", &PyGameData::filterAction)
    		.def("valid_actions", &PyGameData::validActions)
    		.def("update_ram", &PyGameData::updateRam)
    		.def("lookup_all", &PyGameData::lookupAll)
    		.def("get_variable", &PyGameData::getVariable)
    		.def("set_variable", &PyGameData::setVariable)
    		.def("remove_variable", &PyGameData::removeVariable)
    		.def("list_variables", &PyGameData::listVariables)
    		.def("current_reward", &PyGameData::currentReward)
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    		.def("is_done", &PyGameData::isDone)
    		.def("memory", &PyGameData::memory);
    
    Vicki Pfau's avatar
    Vicki Pfau committed
    
    	py::class_<PyMovie>(m, "Movie")
    		.def(py::init<py::str, bool>(), py::arg("path"), py::arg("record") = false)
    		.def("configure", &PyMovie::configure)
    		.def("get_game", &PyMovie::getGameName)
    		.def("step", &PyMovie::step)
    		.def("close", &PyMovie::close)
    		.def("get_key", &PyMovie::getKey)
    		.def("set_key", &PyMovie::setKey)
    		.def("get_state", &PyMovie::getState)
    		.def("set_state", &PyMovie::setState);
    
    	m.def("core_path", &::corePath, py::arg("hint") = py::none());
    	m.def("data_path", &::dataPath, py::arg("hint") = py::none());
    }