openmsx-19.1+dfsg.orig/0000755000175000017500000000000014473433356014363 5ustar shevekshevekopenmsx-19.1+dfsg.orig/src/0000755000175000017500000000000014473433356015152 5ustar shevekshevekopenmsx-19.1+dfsg.orig/src/MSXPPI.cc0000644000175000017500000000750214473433356016505 0ustar shevekshevek#include "MSXPPI.hh" #include "LedStatus.hh" #include "MSXCPUInterface.hh" #include "MSXMotherBoard.hh" #include "Reactor.hh" #include "CassettePort.hh" #include "RenShaTurbo.hh" #include "GlobalSettings.hh" #include "serialize.hh" namespace openmsx { MSXPPI::MSXPPI(const DeviceConfig& config) : MSXDevice(config) , cassettePort(getMotherBoard().getCassettePort()) , renshaTurbo(getMotherBoard().getRenShaTurbo()) , i8255(*this, getCurrentTime(), config.getGlobalSettings().getInvalidPpiModeSetting()) , click(config) , keyboard( config.getMotherBoard(), config.getMotherBoard().getScheduler(), config.getMotherBoard().getCommandController(), config.getMotherBoard().getReactor().getEventDistributor(), config.getMotherBoard().getMSXEventDistributor(), config.getMotherBoard().getStateChangeDistributor(), Keyboard::MATRIX_MSX, config) { reset(getCurrentTime()); } MSXPPI::~MSXPPI() { powerDown(EmuTime::dummy()); } const Keyboard& MSXPPI::getKeyboard() const { return keyboard; } void MSXPPI::reset(EmuTime::param time) { i8255.reset(time); click.reset(time); } void MSXPPI::powerDown(EmuTime::param /*time*/) { getLedStatus().setLed(LedStatus::CAPS, false); } byte MSXPPI::readIO(word port, EmuTime::param time) { return i8255.read(port & 0x03, time); } byte MSXPPI::peekIO(word port, EmuTime::param time) const { return i8255.peek(port & 0x03, time); } void MSXPPI::writeIO(word port, byte value, EmuTime::param time) { i8255.write(port & 0x03, value, time); } // I8255Interface byte MSXPPI::readA(EmuTime::param time) { return peekA(time); } byte MSXPPI::peekA(EmuTime::param /*time*/) const { // port A is normally an output on MSX, reading from an output port // is handled internally in the 8255 // TODO check this on a real MSX // TODO returning 0 fixes the 'get_selected_slot' script right after // reset (when PPI directions are not yet set). For now this // solution is good enough. return 0; } void MSXPPI::writeA(byte value, EmuTime::param /*time*/) { getCPUInterface().setPrimarySlots(value); } byte MSXPPI::readB(EmuTime::param time) { return peekB(time); } byte MSXPPI::peekB(EmuTime::param time) const { auto& keyb = const_cast(keyboard); if (selectedRow != 8) { return keyb.getKeys()[selectedRow]; } else { return keyb.getKeys()[8] | (renshaTurbo.getSignal(time) ? 1:0); } } void MSXPPI::writeB(byte /*value*/, EmuTime::param /*time*/) { // probably nothing happens on a real MSX } nibble MSXPPI::readC1(EmuTime::param time) { return peekC1(time); } nibble MSXPPI::peekC1(EmuTime::param /*time*/) const { return 15; // TODO check this } nibble MSXPPI::readC0(EmuTime::param time) { return peekC0(time); } nibble MSXPPI::peekC0(EmuTime::param /*time*/) const { return 15; // TODO check this } void MSXPPI::writeC1(nibble value, EmuTime::param time) { if ((prevBits ^ value) & 1) { cassettePort.setMotor((value & 1) == 0, time); // 0=0n, 1=Off } if ((prevBits ^ value) & 2) { cassettePort.cassetteOut((value & 2) != 0, time); } if ((prevBits ^ value) & 4) { getLedStatus().setLed(LedStatus::CAPS, (value & 4) == 0); } if ((prevBits ^ value) & 8) { click.setClick((value & 8) != 0, time); } prevBits = value; } void MSXPPI::writeC0(nibble value, EmuTime::param /*time*/) { selectedRow = value; } template void MSXPPI::serialize(Archive& ar, unsigned /*version*/) { ar.template serializeBase(*this); ar.serialize("i8255", i8255); // merge prevBits and selectedRow into one byte auto portC = byte((prevBits << 4) | (selectedRow << 0)); ar.serialize("portC", portC); if constexpr (Archive::IS_LOADER) { selectedRow = (portC >> 0) & 0xF; nibble bits = (portC >> 4) & 0xF; writeC1(bits, getCurrentTime()); } ar.serialize("keyboard", keyboard); } INSTANTIATE_SERIALIZE_METHODS(MSXPPI); REGISTER_MSXDEVICE(MSXPPI, "PPI"); } // namespace openmsx openmsx-19.1+dfsg.orig/src/Autofire.hh0000644000175000017500000000456114473433356017257 0ustar shevekshevek#ifndef AUTOFIRE_HH #define AUTOFIRE_HH #include "Observer.hh" #include "DynamicClock.hh" #include "EmuTime.hh" #include "IntegerSetting.hh" #include "StateChangeListener.hh" #include "static_string_view.hh" namespace openmsx { class MSXMotherBoard; class Scheduler; class StateChangeDistributor; /** * Autofire is a device that is between two other devices and outside * the bus. For example, between the keyboard and the PPI * or between a joyport connecter and the PSG. * * There can be multiple autofire circuits. For example, one used * by the Ren-Sha Turbo and another one built into a joystick. */ class Autofire final : private Observer, private StateChangeListener { public: enum ID { RENSHATURBO, UNKNOWN }; public: Autofire(MSXMotherBoard& motherBoard, unsigned newMinInts, unsigned newMaxInts, ID id); ~Autofire(); /** Get the output signal in negative logic. * @result When auto-fire is on, result will alternate between true * and false. When auto-fire if off result is false. */ [[nodiscard]] bool getSignal(EmuTime::param time); template void serialize(Archive& ar, unsigned version); private: void setSpeed(EmuTime::param time); /** Sets the clock frequency according to the current value of the speed * settings. */ void setClock(int speed); // Observer void update(const Setting& setting) noexcept override; // StateChangeListener void signalStateChange(const StateChange& event) override; void stopReplay(EmuTime::param time) noexcept override; private: Scheduler& scheduler; StateChangeDistributor& stateChangeDistributor; // Following two values specify the range of the autofire // as measured by the test program: /** Number of interrupts at fastest setting (>=1). * The number of interrupts for 50 periods, measured * in ntsc mode (which gives 60 interrupts per second). */ const unsigned min_ints; /** Number of interrupts at slowest setting (>=min_ints+1). * The number of interrupts for 50 periods, measured * in ntsc mode (which gives 60 interrupts per second). */ const unsigned max_ints; /** The currently selected speed. */ IntegerSetting speedSetting; /** Each tick of this clock, the signal changes. * Frequency is derived from speed, min_ints and max_ints. */ DynamicClock clock; const ID id; }; } // namespace openmsx #endif openmsx-19.1+dfsg.orig/src/MSXMatsushita.hh0000644000175000017500000000304514473433356020207 0ustar shevekshevek#ifndef MSXMATSUSHITA_HH #define MSXMATSUSHITA_HH #include "EmuTime.hh" #include "MSXDevice.hh" #include "MSXSwitchedDevice.hh" #include "FirmwareSwitch.hh" #include "Clock.hh" #include "serialize_meta.hh" namespace openmsx { class MSXCPU; class SRAM; class VDP; class MSXMatsushita final : public MSXDevice, public MSXSwitchedDevice { public: explicit MSXMatsushita(const DeviceConfig& config); void init() override; ~MSXMatsushita() override; // MSXDevice void reset(EmuTime::param time) override; [[nodiscard]] byte readIO(word port, EmuTime::param time) override; [[nodiscard]] byte peekIO(word port, EmuTime::param time) const override; void writeIO(word port, byte value, EmuTime::param time) override; // MSXSwitchedDevice [[nodiscard]] byte readSwitchedIO(word port, EmuTime::param time) override; [[nodiscard]] byte peekSwitchedIO(word port, EmuTime::param time) const override; void writeSwitchedIO(word port, byte value, EmuTime::param time) override; template void serialize(Archive& ar, unsigned version); private: void unwrap(); void delay(EmuTime::param time); private: MSXCPU& cpu; VDP* vdp = nullptr; /** Remembers the time at which last VDP I/O action took place. */ Clock<5369318> lastTime{EmuTime::zero()}; // 5.3MHz = 3.5MHz * 3/2 FirmwareSwitch firmwareSwitch; const std::unique_ptr sram; // can be nullptr word address; nibble color1, color2; byte pattern; const bool turboAvailable; bool turboEnabled = false; }; SERIALIZE_CLASS_VERSION(MSXMatsushita, 2); } // namespace openmsx #endif openmsx-19.1+dfsg.orig/src/RP5C01.cc0000644000175000017500000002064414473433356016341 0ustar shevekshevek#include "RP5C01.hh" #include "SRAM.hh" #include "narrow.hh" #include "one_of.hh" #include "serialize.hh" #include #include #include namespace openmsx { // TODO ALARM is not implemented (not connected on MSX) // TODO 1Hz 16Hz output not implemented (not connected on MSX) static constexpr nibble MODE_REG = 13; static constexpr nibble TEST_REG = 14; static constexpr nibble RESET_REG = 15; static constexpr nibble TIME_BLOCK = 0; static constexpr nibble ALARM_BLOCK = 1; static constexpr nibble MODE_BLOK_SELECT = 0x3; static constexpr nibble MODE_ALARM_ENABLE = 0x4; static constexpr nibble MODE_TIMER_ENABLE = 0x8; static constexpr nibble TEST_SECONDS = 0x1; static constexpr nibble TEST_MINUTES = 0x2; static constexpr nibble TEST_DAYS = 0x4; static constexpr nibble TEST_YEARS = 0x8; static constexpr nibble RESET_ALARM = 0x1; static constexpr nibble RESET_FRACTION = 0x2; // 0-bits are ignored on writing and return 0 on reading static constexpr std::array mask = { std::array{0xf, 0x7, 0xf, 0x7, 0xf, 0x3, 0x7, 0xf, 0x3, 0xf, 0x1, 0xf, 0xf}, std::array{0x0, 0x0, 0xf, 0x7, 0xf, 0x3, 0x7, 0xf, 0x3, 0x0, 0x1, 0x3, 0x0}, std::array{0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf}, std::array{0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf}, }; RP5C01::RP5C01(CommandController& commandController, SRAM& regs_, EmuTime::param time, const std::string& name) : regs(regs_) , modeSetting( commandController, ((name == "Real time clock") ? std::string_view("rtcmode") // bw-compat : tmpStrCat(name + " mode")), "Real Time Clock mode", RP5C01::EMUTIME, EnumSetting::Map{ {"EmuTime", RP5C01::EMUTIME}, {"RealTime", RP5C01::REALTIME}}) , reference(time) { initializeTime(); reset(time); } void RP5C01::reset(EmuTime::param time) { modeReg = MODE_TIMER_ENABLE; testReg = 0; resetReg = 0; updateTimeRegs(time); } nibble RP5C01::readPort(nibble port, EmuTime::param time) { switch (port) { case MODE_REG: case TEST_REG: case RESET_REG: // nothing break; default: unsigned block = modeReg & MODE_BLOK_SELECT; if (block == one_of(TIME_BLOCK, ALARM_BLOCK)) { updateTimeRegs(time); } } return peekPort(port); } nibble RP5C01::peekPort(nibble port) const { assert(port <= 0x0f); switch (port) { case MODE_REG: return modeReg; case TEST_REG: case RESET_REG: // write only return 0x0f; // TODO check this default: unsigned block = modeReg & MODE_BLOK_SELECT; nibble tmp = regs[block * 13 + port]; return tmp & mask[block][port]; } } void RP5C01::writePort(nibble port, nibble value, EmuTime::param time) { assert (port <= 0x0f); switch (port) { case MODE_REG: updateTimeRegs(time); modeReg = value; break; case TEST_REG: updateTimeRegs(time); testReg = value; break; case RESET_REG: resetReg = value; if (value & RESET_ALARM) { resetAlarm(); } if (value & RESET_FRACTION) { fraction = 0; } break; default: unsigned block = modeReg & MODE_BLOK_SELECT; if (block == one_of(TIME_BLOCK, ALARM_BLOCK)) { updateTimeRegs(time); } regs.write(block * 13 + port, value & mask[block][port]); if (block == one_of(TIME_BLOCK, ALARM_BLOCK)) { regs2Time(); } } } void RP5C01::initializeTime() { time_t t = time(nullptr); struct tm *tm = localtime(&t); fraction = 0; // fractions of a second seconds = tm->tm_sec; // 0-59 minutes = tm->tm_min; // 0-59 hours = tm->tm_hour; // 0-23 dayWeek = tm->tm_wday; // 0-6 0=sunday days = tm->tm_mday-1; // 0-30 months = tm->tm_mon; // 0-11 years = tm->tm_year - 80; // 0-99 0=1980 leapYear = tm->tm_year % 4; // 0-3 0=leap year time2Regs(); } void RP5C01::regs2Time() { seconds = regs[TIME_BLOCK * 13 + 0] + 10 * regs[TIME_BLOCK * 13 + 1]; minutes = regs[TIME_BLOCK * 13 + 2] + 10 * regs[TIME_BLOCK * 13 + 3]; hours = regs[TIME_BLOCK * 13 + 4] + 10 * regs[TIME_BLOCK * 13 + 5]; dayWeek = regs[TIME_BLOCK * 13 + 6]; days = regs[TIME_BLOCK * 13 + 7] + 10 * regs[TIME_BLOCK * 13 + 8] - 1; months = regs[TIME_BLOCK * 13 + 9] + 10 * regs[TIME_BLOCK * 13 +10] - 1; years = regs[TIME_BLOCK * 13 +11] + 10 * regs[TIME_BLOCK * 13 +12]; leapYear = regs[ALARM_BLOCK * 13 +11]; if (!regs[ALARM_BLOCK * 13 + 10]) { // 12 hours mode if (hours >= 20) hours = (hours - 20) + 12; } } void RP5C01::time2Regs() { unsigned hours_ = hours; if (!regs[ALARM_BLOCK * 13 + 10]) { // 12 hours mode if (hours >= 12) hours_ = (hours - 12) + 20; } regs.write(TIME_BLOCK * 13 + 0, narrow( seconds % 10)); regs.write(TIME_BLOCK * 13 + 1, narrow( seconds / 10)); regs.write(TIME_BLOCK * 13 + 2, narrow( minutes % 10)); regs.write(TIME_BLOCK * 13 + 3, narrow( minutes / 10)); regs.write(TIME_BLOCK * 13 + 4, narrow( hours_ % 10)); regs.write(TIME_BLOCK * 13 + 5, narrow( hours_ / 10)); regs.write(TIME_BLOCK * 13 + 6, narrow( dayWeek)); regs.write(TIME_BLOCK * 13 + 7, narrow((days+1) % 10)); // 0-30 -> 1-31 regs.write(TIME_BLOCK * 13 + 8, narrow((days+1) / 10)); // 0-11 -> 1-12 regs.write(TIME_BLOCK * 13 + 9, narrow((months+1) % 10)); regs.write(TIME_BLOCK * 13 + 10, narrow((months+1) / 10)); regs.write(TIME_BLOCK * 13 + 11, narrow( years % 10)); regs.write(TIME_BLOCK * 13 + 12, narrow( years / 10)); regs.write(ALARM_BLOCK * 13 + 11, narrow( leapYear)); } static constexpr int daysInMonth(int month, unsigned leapYear) { constexpr std::array daysInMonths = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; month %= 12; return ((month == 1) && (leapYear == 0)) ? 29 : daysInMonths[month]; } void RP5C01::updateTimeRegs(EmuTime::param time) { if (modeSetting.getEnum() == EMUTIME) { // sync with EmuTime, perfect emulation auto elapsed = reference.getTicksTill(time); reference.advance(time); // in test mode increase sec/min/.. at a rate of 16384Hz fraction += (modeReg & MODE_TIMER_ENABLE) ? elapsed : 0; unsigned carrySeconds = (testReg & TEST_SECONDS) ? elapsed : fraction / FREQ; seconds += carrySeconds; unsigned carryMinutes = (testReg & TEST_MINUTES) ? elapsed : seconds / 60; minutes += carryMinutes; hours += minutes / 60; unsigned carryDays = (testReg & TEST_DAYS) ? elapsed : hours / 24; if (carryDays) { // Only correct for number of days in a month when we // actually advance the day. Because otherwise e.g. the // following scenario goes wrong: // - Suppose current date is 'xx/07/31' and we want to // change that to 'xx/12/31'. // - Changing the months is done in two steps: first the // lower then the higher nibble. // - So temporary we go via the (invalid) date 'xx/02/31' // (february does not have 32 days) // - We must NOT roll over the days and advance to march. days += narrow_cast(carryDays); dayWeek += carryDays; while (days >= daysInMonth(months, leapYear)) { // TODO not correct because leapYear is not updated // is only triggered when we update several months // at a time (but might happen in TEST_DAY mode) days -= daysInMonth(months, leapYear); months++; } } unsigned carryYears = (testReg & TEST_YEARS) ? elapsed : unsigned(months / 12); years += carryYears; leapYear += carryYears; fraction %= FREQ; seconds %= 60; minutes %= 60; hours %= 24; dayWeek %= 7; months %= 12; years %= 100; leapYear %= 4; time2Regs(); } else { // sync with host clock // writes to time, test and reset registers have no effect initializeTime(); } } void RP5C01::resetAlarm() { for (auto i : xrange(2, 9)) { regs.write(ALARM_BLOCK * 13 + i, 0); } } template void RP5C01::serialize(Archive& ar, unsigned /*version*/) { ar.serialize("reference", reference, "fraction", fraction, "seconds", seconds, "minutes", minutes, "hours", hours, "dayWeek", dayWeek, "years", years, "leapYear", leapYear, "days", days, "months", months, "modeReg", modeReg, "testReg", testReg, "resetReg", resetReg); } INSTANTIATE_SERIALIZE_METHODS(RP5C01); } // namespace openmsx openmsx-19.1+dfsg.orig/src/DeviceFactory.cc0000644000175000017500000002734614473433356020224 0ustar shevekshevek#include "DeviceFactory.hh" #include "XMLElement.hh" #include "DeviceConfig.hh" #include "ChakkariCopy.hh" #include "CanonWordProcessor.hh" #include "FraelSwitchableROM.hh" #include "MSXRam.hh" #include "MSXPPI.hh" #include "SVIPPI.hh" #include "VDP.hh" #include "MSXE6Timer.hh" #include "MSXFacMidiInterface.hh" #include "MSXHiResTimer.hh" #include "MSXResetStatusRegister.hh" #include "MSXTurboRPause.hh" #include "MSXTurboRPCM.hh" #include "MSXS1985.hh" #include "MSXS1990.hh" #include "ColecoJoystickIO.hh" #include "ColecoSuperGameModule.hh" #include "SG1000JoystickIO.hh" #include "SG1000Pause.hh" #include "SC3000PPI.hh" #include "MSXPSG.hh" #include "SVIPSG.hh" #include "SNPSG.hh" #include "MSXMusic.hh" #include "MSXFmPac.hh" #include "MSXAudio.hh" #include "MSXMoonSound.hh" #include "MSXOPL3Cartridge.hh" #include "MSXYamahaSFG.hh" #include "MusicModuleMIDI.hh" #include "JVCMSXMIDI.hh" #include "MSXKanji.hh" #include "MSXBunsetsu.hh" #include "MSXMemoryMapper.hh" #include "MSXToshibaTcx200x.hh" #include "MegaFlashRomSCCPlusSD.hh" #include "MusicalMemoryMapper.hh" #include "Carnivore2.hh" #include "PanasonicRam.hh" #include "MSXRTC.hh" #include "PasswordCart.hh" #include "RomFactory.hh" #include "MSXPrinterPort.hh" #include "SVIPrinterPort.hh" #include "MSXSCCPlusCart.hh" #include "PhilipsFDC.hh" #include "MicrosolFDC.hh" #include "AVTFDC.hh" #include "NationalFDC.hh" #include "VictorFDC.hh" #include "SanyoFDC.hh" #include "ToshibaFDC.hh" #include "CanonFDC.hh" #include "SpectravideoFDC.hh" #include "TalentTDC600.hh" #include "TurboRFDC.hh" #include "SVIFDC.hh" #include "YamahaFDC.hh" #include "SunriseIDE.hh" #include "BeerIDE.hh" #include "GoudaSCSI.hh" #include "MegaSCSI.hh" #include "ESE_RAM.hh" #include "ESE_SCC.hh" #include "MSXMatsushita.hh" #include "MSXVictorHC9xSystemControl.hh" #include "MSXCielTurbo.hh" #include "MSXKanji12.hh" #include "MSXMidi.hh" #include "MSXRS232.hh" #include "MSXMegaRam.hh" #include "MSXPac.hh" #include "MSXHBI55.hh" #include "DebugDevice.hh" #include "V9990.hh" #include "Video9000.hh" #include "ADVram.hh" #include "NowindInterface.hh" #include "MSXMirrorDevice.hh" #include "DummyDevice.hh" #include "MSXDeviceSwitch.hh" #include "MSXMapperIO.hh" #include "VDPIODelay.hh" #include "SensorKid.hh" #include "YamahaSKW01.hh" #include "CliComm.hh" #include "MSXException.hh" #include "components.hh" #include "one_of.hh" #include #if COMPONENT_LASERDISC #include "PioneerLDControl.hh" #endif using std::make_unique; namespace openmsx { [[nodiscard]] static std::unique_ptr createWD2793BasedFDC(const DeviceConfig& conf) { const auto* styleEl = conf.findChild("connectionstyle"); std::string type; if (!styleEl) { conf.getCliComm().printWarning( "WD2793 as FDC type without a connectionstyle is " "deprecated, please update your config file to use " "WD2793 with connectionstyle Philips!"); type = "Philips"; } else { type = styleEl->getData(); } if (type == one_of("Philips", "Sony")) { return make_unique(conf); } else if (type == "Microsol") { return make_unique(conf); } else if (type == "AVT") { return make_unique(conf); } else if (type == "National") { return make_unique(conf); } else if (type == "Sanyo") { return make_unique(conf); } else if (type == "Toshiba") { return make_unique(conf); } else if (type == "Canon") { return make_unique(conf); } else if (type == "Spectravideo") { return make_unique(conf); } else if (type == "Victor") { return make_unique(conf); } else if (type == "Yamaha") { return make_unique(conf); } throw MSXException("Unknown WD2793 FDC connection style ", type); } std::unique_ptr DeviceFactory::create(const DeviceConfig& conf) { std::unique_ptr result; const auto& type = conf.getXML()->getName(); if (type == "PPI") { result = make_unique(conf); } else if (type == "SVIPPI") { result = make_unique(conf); } else if (type == "RAM") { result = make_unique(conf); } else if (type == "VDP") { result = make_unique(conf); } else if (type == "E6Timer") { result = make_unique(conf); } else if (type == "HiResTimer") { result = make_unique(conf); } else if (type == one_of("ResetStatusRegister", "F4Device")) { result = make_unique(conf); } else if (type == "TurboRPause") { result = make_unique(conf); } else if (type == "TurboRPCM") { result = make_unique(conf); } else if (type == "S1985") { result = make_unique(conf); } else if (type == "S1990") { result = make_unique(conf); } else if (type == "ColecoJoystick") { result = make_unique(conf); } else if (type == "SuperGameModule") { result = make_unique(conf); } else if (type == "SG1000Joystick") { result = make_unique(conf); } else if (type == "SG1000Pause") { result = make_unique(conf); } else if (type == "SC3000PPI") { result = make_unique(conf); } else if (type == "PSG") { result = make_unique(conf); } else if (type == "SVIPSG") { result = make_unique(conf); } else if (type == "SNPSG") { result = make_unique(conf); } else if (type == "MSX-MUSIC") { result = make_unique(conf); } else if (type == "MSX-MUSIC-WX") { result = make_unique(conf); } else if (type == "FMPAC") { result = make_unique(conf); } else if (type == "MSX-AUDIO") { result = make_unique(conf); } else if (type == "MusicModuleMIDI") { result = make_unique(conf); } else if (type == "JVCMSXMIDI") { result = make_unique(conf); } else if (type == "FACMIDIInterface") { result = make_unique(conf); } else if (type == "YamahaSFG") { result = make_unique(conf); } else if (type == "MoonSound") { result = make_unique(conf); } else if (type == "OPL3Cartridge") { result = make_unique(conf); } else if (type == "Kanji") { result = make_unique(conf); } else if (type == "Bunsetsu") { result = make_unique(conf); } else if (type == "MemoryMapper") { result = make_unique(conf); } else if (type == "PanasonicRAM") { result = make_unique(conf); } else if (type == "RTC") { result = make_unique(conf); } else if (type == "PasswordCart") { result = make_unique(conf); } else if (type == "ROM") { result = RomFactory::create(conf); } else if (type == "PrinterPort") { result = make_unique(conf); } else if (type == "SVIPrinterPort") { result = make_unique(conf); } else if (type == "SCCplus") { // Note: it's actually called SCC-I result = make_unique(conf); } else if (type == one_of("WD2793", "WD1770")) { result = createWD2793BasedFDC(conf); } else if (type == "Microsol") { conf.getCliComm().printWarning( "Microsol as FDC type is deprecated, please update " "your config file to use WD2793 with connectionstyle " "Microsol!"); result = make_unique(conf); } else if (type == "MB8877A") { conf.getCliComm().printWarning( "MB8877A as FDC type is deprecated, please update your " "config file to use WD2793 with connectionstyle National!"); result = make_unique(conf); } else if (type == "TC8566AF") { result = make_unique(conf); } else if (type == "TDC600") { result = make_unique(conf); } else if (type == "ToshibaTCX-200x") { result = make_unique(conf); } else if (type == "SVIFDC") { result = make_unique(conf); } else if (type == "BeerIDE") { result = make_unique(conf); } else if (type == "SunriseIDE") { result = make_unique(conf); } else if (type == "GoudaSCSI") { result = make_unique(conf); } else if (type == "MegaSCSI") { result = make_unique(conf); } else if (type == "ESERAM") { result = make_unique(conf); } else if (type == "WaveSCSI") { result = make_unique(conf, true); } else if (type == "ESESCC") { result = make_unique(conf, false); } else if (type == "Matsushita") { result = make_unique(conf); } else if (type == "VictorHC9xSystemControl") { result = make_unique(conf); } else if (type == "CielTurbo") { result = make_unique(conf); } else if (type == "Kanji12") { result = make_unique(conf); } else if (type == "MSX-MIDI") { result = make_unique(conf); } else if (type == "MSX-RS232") { result = make_unique(conf); } else if (type == "MegaRam") { result = make_unique(conf); } else if (type == "PAC") { result = make_unique(conf); } else if (type == "HBI55") { result = make_unique(conf); } else if (type == "DebugDevice") { result = make_unique(conf); } else if (type == "V9990") { result = make_unique(conf); } else if (type == "Video9000") { result = make_unique(conf); } else if (type == "ADVram") { result = make_unique(conf); } else if (type == "PioneerLDControl") { #if COMPONENT_LASERDISC result = make_unique(conf); #else throw MSXException("Laserdisc component not compiled in"); #endif } else if (type == "Nowind") { result = make_unique(conf); } else if (type == "Mirror") { result = make_unique(conf); } else if (type == "SensorKid") { result = make_unique(conf); } else if (type == "FraelSwitchableROM") { result = make_unique(conf); } else if (type == "ChakkariCopy") { result = make_unique(conf); } else if (type == "CanonWordProcessor") { result = make_unique(conf); } else if (type == "MegaFlashRomSCCPlusSD") { result = make_unique(conf); } else if (type == "MusicalMemoryMapper") { result = make_unique(conf); } else if (type == "Carnivore2") { result = make_unique(conf); } else if (type == "YamahaSKW01") { result = make_unique(conf); } else if (type == one_of("T7775", "T7937", "T9763", "T9769")) { // Ignore for now. We might want to create a real device for it later. } else { throw MSXException("Unknown device \"", type, "\" specified in configuration"); } if (result) result->init(); return result; } [[nodiscard]] static XMLElement& createConfig(const char* name, const char* id) { auto& doc = XMLDocument::getStaticDocument(); auto* config = doc.allocateElement(name); config->setFirstAttribute(doc.allocateAttribute("id", id)); return *config; } std::unique_ptr DeviceFactory::createDummyDevice( const HardwareConfig& hwConf) { static const XMLElement& xml(createConfig("Dummy", "")); return make_unique(DeviceConfig(hwConf, xml)); } std::unique_ptr DeviceFactory::createDeviceSwitch( const HardwareConfig& hwConf) { static const XMLElement& xml(createConfig("DeviceSwitch", "DeviceSwitch")); return make_unique(DeviceConfig(hwConf, xml)); } std::unique_ptr DeviceFactory::createMapperIO( const HardwareConfig& hwConf) { static const XMLElement& xml(createConfig("MapperIO", "MapperIO")); return make_unique(DeviceConfig(hwConf, xml)); } std::unique_ptr DeviceFactory::createVDPIODelay( const HardwareConfig& hwConf, MSXCPUInterface& cpuInterface) { static const XMLElement& xml(createConfig("VDPIODelay", "VDPIODelay")); return make_unique(DeviceConfig(hwConf, xml), cpuInterface); } } // namespace openmsx openmsx-19.1+dfsg.orig/src/ReverseManager.cc0000644000175000017500000010276014473433356020375 0ustar shevekshevek#include "ReverseManager.hh" #include "Event.hh" #include "MSXMotherBoard.hh" #include "EventDistributor.hh" #include "StateChangeDistributor.hh" #include "Keyboard.hh" #include "Debugger.hh" #include "EventDelay.hh" #include "MSXMixer.hh" #include "MSXCommandController.hh" #include "XMLException.hh" #include "TclArgParser.hh" #include "TclObject.hh" #include "FileOperations.hh" #include "FileContext.hh" #include "StateChange.hh" #include "Timer.hh" #include "CliComm.hh" #include "Display.hh" #include "Reactor.hh" #include "CommandException.hh" #include "MemBuffer.hh" #include "narrow.hh" #include "one_of.hh" #include "ranges.hh" #include "serialize.hh" #include "serialize_meta.hh" #include "view.hh" #include #include #include #include namespace openmsx { // Time between two snapshots (in seconds) static constexpr double SNAPSHOT_PERIOD = 1.0; // Max number of snapshots in a replay file static constexpr unsigned MAX_NOF_SNAPSHOTS = 10; // Min distance between snapshots in replay file (in seconds) static constexpr auto MIN_PARTITION_LENGTH = EmuDuration(60.0); // Max distance of one before last snapshot before the end time in replay file (in seconds) static constexpr auto MAX_DIST_1_BEFORE_LAST_SNAPSHOT = EmuDuration(30.0); static constexpr const char* const REPLAY_DIR = "replays"; // A replay is a struct that contains a vector of motherboards and an MSX event // log. Those combined are a replay, because you can replay the events from an // existing motherboard state: the vector has to have at least one motherboard // (the initial state), but can have optionally more motherboards, which are // merely in-between snapshots, so it is quicker to jump to a later time in the // event log. struct Replay { explicit Replay(Reactor& reactor_) : reactor(reactor_), currentTime(EmuTime::dummy()) {} Reactor& reactor; ReverseManager::Events* events; std::vector motherBoards; EmuTime currentTime; // this is the amount of times the reverse goto command was used, which // is interesting for the TAS community (see tasvideos.org). It's an // indication of the effort it took to create the replay. Note that // there is no way to verify this number. unsigned reRecordCount; template void serialize(Archive& ar, unsigned version) { if (ar.versionAtLeast(version, 2)) { ar.serializeWithID("snapshots", motherBoards, std::ref(reactor)); } else { Reactor::Board newBoard = reactor.createEmptyMotherBoard(); ar.serialize("snapshot", *newBoard); motherBoards.push_back(std::move(newBoard)); } ar.serialize("events", *events); if (ar.versionAtLeast(version, 3)) { ar.serialize("currentTime", currentTime); } else { assert(Archive::IS_LOADER); assert(!events->empty()); currentTime = events->back()->getTime(); } if (ar.versionAtLeast(version, 4)) { ar.serialize("reRecordCount", reRecordCount); } } }; SERIALIZE_CLASS_VERSION(Replay, 4); // struct ReverseHistory void ReverseManager::ReverseHistory::swap(ReverseHistory& other) noexcept { std::swap(chunks, other.chunks); std::swap(events, other.events); } void ReverseManager::ReverseHistory::clear() { // clear() and free storage capacity Chunks().swap(chunks); Events().swap(events); } class EndLogEvent final : public StateChange { public: EndLogEvent() = default; // for serialize explicit EndLogEvent(EmuTime::param time_) : StateChange(time_) { } template void serialize(Archive& ar, unsigned /*version*/) { ar.template serializeBase(*this); } }; REGISTER_POLYMORPHIC_CLASS(StateChange, EndLogEvent, "EndLog"); // class ReverseManager ReverseManager::ReverseManager(MSXMotherBoard& motherBoard_) : syncNewSnapshot(motherBoard_.getScheduler()) , syncInputEvent (motherBoard_.getScheduler()) , motherBoard(motherBoard_) , eventDistributor(motherBoard.getReactor().getEventDistributor()) , reverseCmd(motherBoard.getCommandController()) { eventDistributor.registerEventListener(EventType::TAKE_REVERSE_SNAPSHOT, *this); assert(!isCollecting()); assert(!isReplaying()); } ReverseManager::~ReverseManager() { stop(); eventDistributor.unregisterEventListener(EventType::TAKE_REVERSE_SNAPSHOT, *this); } bool ReverseManager::isReplaying() const { return replayIndex != history.events.size(); } void ReverseManager::start() { if (!isCollecting()) { // create first snapshot collecting = true; takeSnapshot(getCurrentTime()); // schedule creation of next snapshot schedule(getCurrentTime()); // start recording events motherBoard.getStateChangeDistributor().registerRecorder(*this); } assert(isCollecting()); } void ReverseManager::stop() { if (isCollecting()) { motherBoard.getStateChangeDistributor().unregisterRecorder(*this); syncNewSnapshot.removeSyncPoint(); // don't schedule new snapshot takings syncInputEvent .removeSyncPoint(); // stop any pending replay actions history.clear(); replayIndex = 0; collecting = false; pendingTakeSnapshot = false; } assert(!pendingTakeSnapshot); assert(!isCollecting()); assert(!isReplaying()); } EmuTime::param ReverseManager::getEndTime(const ReverseHistory& hist) const { if (!hist.events.empty()) { if (const auto* ev = dynamic_cast( hist.events.back().get())) { // last log element is EndLogEvent, use that return ev->getTime(); } } // otherwise use current time assert(!isReplaying()); return getCurrentTime(); } void ReverseManager::status(TclObject& result) const { result.addDictKeyValue("status", !isCollecting() ? "disabled" : isReplaying() ? "replaying" : "enabled"); EmuTime b(isCollecting() ? begin(history.chunks)->second.time : EmuTime::zero()); result.addDictKeyValue("begin", (b - EmuTime::zero()).toDouble()); EmuTime end(isCollecting() ? getEndTime(history) : EmuTime::zero()); result.addDictKeyValue("end", (end - EmuTime::zero()).toDouble()); EmuTime current(isCollecting() ? getCurrentTime() : EmuTime::zero()); result.addDictKeyValue("current", (current - EmuTime::zero()).toDouble()); TclObject snapshots; snapshots.addListElements(view::transform(history.chunks, [](auto& p) { return (p.second.time - EmuTime::zero()).toDouble(); })); result.addDictKeyValue("snapshots", snapshots); auto lastEvent = rbegin(history.events); if (lastEvent != rend(history.events) && dynamic_cast(lastEvent->get())) { ++lastEvent; } EmuTime le(isCollecting() && (lastEvent != rend(history.events)) ? (*lastEvent)->getTime() : EmuTime::zero()); result.addDictKeyValue("last_event", (le - EmuTime::zero()).toDouble()); } void ReverseManager::debugInfo(TclObject& result) const { // TODO this is useful during development, but for the end user this // information means nothing. We should remove this later. std::string res; size_t totalSize = 0; for (const auto& [idx, chunk] : history.chunks) { strAppend(res, idx, ' ', (chunk.time - EmuTime::zero()).toDouble(), ' ', ((chunk.time - EmuTime::zero()).toDouble() / (getCurrentTime() - EmuTime::zero()).toDouble()) * 100, "%" " (", chunk.size, ")" " (next event index: ", chunk.eventCount, ")\n"); totalSize += chunk.size; } strAppend(res, "total size: ", totalSize, '\n'); result = res; } static std::pair parseGoTo(Interpreter& interp, std::span tokens) { bool noVideo = false; std::array info = {flagArg("-novideo", noVideo)}; auto args = parseTclArgs(interp, tokens.subspan(2), info); if (args.size() != 1) throw SyntaxError(); double time = args[0].getDouble(interp); return {noVideo, time}; } void ReverseManager::goBack(std::span tokens) { auto& interp = motherBoard.getReactor().getInterpreter(); auto [noVideo, t] = parseGoTo(interp, tokens); EmuTime now = getCurrentTime(); EmuTime target(EmuTime::dummy()); if (t >= 0) { EmuDuration d(t); if (d < (now - EmuTime::zero())) { target = now - d; } else { target = EmuTime::zero(); } } else { target = now + EmuDuration(-t); } goTo(target, noVideo); } void ReverseManager::goTo(std::span tokens) { auto& interp = motherBoard.getReactor().getInterpreter(); auto [noVideo, t] = parseGoTo(interp, tokens); EmuTime target = EmuTime::zero() + EmuDuration(t); goTo(target, noVideo); } void ReverseManager::goTo(EmuTime::param target, bool noVideo) { if (!isCollecting()) { throw CommandException( "Reverse was not enabled. First execute the 'reverse " "start' command to start collecting data."); } goTo(target, noVideo, history, true); // move in current time-line } // this function is used below, but factored out, because it's already way too long static void reportProgress(Reactor& reactor, const EmuTime& targetTime, unsigned percentage) { double targetTimeDisp = (targetTime - EmuTime::zero()).toDouble(); std::ostringstream sstr; sstr << "Time warping to " << int(targetTimeDisp / 60) << ':' << std::setfill('0') << std::setw(5) << std::setprecision(2) << std::fixed << std::fmod(targetTimeDisp, 60.0) << "... " << percentage << '%'; reactor.getCliComm().printProgress(sstr.str()); reactor.getDisplay().repaint(); } void ReverseManager::goTo( EmuTime::param target, bool noVideo, ReverseHistory& hist, bool sameTimeLine) { auto& mixer = motherBoard.getMSXMixer(); try { // The call to MSXMotherBoard::fastForward() below may take // some time to execute. The DirectX sound driver has a problem // (not easily fixable) that it keeps on looping the sound // buffer on buffer underruns (the SDL driver plays silence on // underrun). At the end of this function we will switch to a // different active MSXMotherBoard. So we can as well now // already mute the current MSXMotherBoard. mixer.mute(); // -- Locate destination snapshot -- // We can't go back further in the past than the first snapshot. assert(!hist.chunks.empty()); auto it = begin(hist.chunks); EmuTime firstTime = it->second.time; EmuTime targetTime = std::max(target, firstTime); // Also don't go further into the future than 'end time'. targetTime = std::min(targetTime, getEndTime(hist)); // Duration of 2 PAL frames. Possible improvement is to use the // actual refresh rate (PAL/NTSC). But it should be the refresh // rate of the active video chip (v99x8/v9990) at the target // time. This is quite complex to get and the difference between // 2 PAL and 2 NTSC frames isn't that big. double dur2frames = 2.0 * (313.0 * 1368.0) / (3579545.0 * 6.0); EmuDuration preDelta(noVideo ? 0.0 : dur2frames); EmuTime preTarget = ((targetTime - firstTime) > preDelta) ? targetTime - preDelta : firstTime; // find oldest snapshot that is not newer than requested time // TODO ATM we do a linear search, could be improved to do a binary search. assert(it->second.time <= preTarget); // first one is not newer assert(it != end(hist.chunks)); // there are snapshots do { ++it; } while (it != end(hist.chunks) && it->second.time <= preTarget); // We found the first one that's newer, previous one is last // one that's not newer (thus older or equal). assert(it != begin(hist.chunks)); --it; ReverseChunk& chunk = it->second; EmuTime snapshotTime = chunk.time; assert(snapshotTime <= preTarget); // IF current time is before the wanted time AND either // - current time is closer than the closest (earlier) snapshot // - OR current time is close enough (I arbitrarily choose 1s) // THEN it's cheaper to start from the current position (and // emulated forward) than to start from a snapshot // THOUGH only when we're currently in the same time-line // e.g. OK for a 'reverse goto' command, but not for a // 'reverse loadreplay' command. auto& reactor = motherBoard.getReactor(); EmuTime currentTime = getCurrentTime(); MSXMotherBoard* newBoard; Reactor::Board newBoard_; // either nullptr or the same as newBoard if (sameTimeLine && (currentTime <= preTarget) && ((snapshotTime <= currentTime) || ((preTarget - currentTime) < EmuDuration(1.0)))) { newBoard = &motherBoard; // use current board } else { // Note: we don't (anymore) erase future snapshots // -- restore old snapshot -- newBoard_ = reactor.createEmptyMotherBoard(); newBoard = newBoard_.get(); MemInputArchive in(chunk.savestate.data(), chunk.size, chunk.deltaBlocks); in.serialize("machine", *newBoard); if (eventDelay) { // Handle all events that are scheduled, but not yet // distributed. This makes sure no events get lost // (important to keep host/msx keyboard in sync). eventDelay->flush(); } // terminate replay log with EndLogEvent (if not there already) if (hist.events.empty() || !dynamic_cast(hist.events.back().get())) { hist.events.push_back( std::make_unique(currentTime)); } // Transfer history to the new ReverseManager. // Also we should stop collecting in this ReverseManager, // and start collecting in the new one. auto& newManager = newBoard->getReverseManager(); newManager.transferHistory(hist, chunk.eventCount); // transfer (or copy) state from old to new machine transferState(*newBoard); // In case of load-replay it's possible we are not collecting, // but calling stop() anyway is ok. stop(); } // -- goto correct time within snapshot -- // Fast forward 2 frames before target time. // If we're short on snapshots, create them at intervals that are // at least the usual interval, but the later, the more: each // time divide the remaining time in half and make a snapshot // there. auto lastProgress = Timer::getTime(); auto startMSXTime = newBoard->getCurrentTime(); auto lastSnapshotTarget = startMSXTime; bool everShowedProgress = false; syncNewSnapshot.removeSyncPoint(); // don't schedule new snapshot takings during fast forward while (true) { auto currentTimeNewBoard = newBoard->getCurrentTime(); auto nextSnapshotTarget = std::min( preTarget, lastSnapshotTarget + std::max( EmuDuration(SNAPSHOT_PERIOD), (preTarget - lastSnapshotTarget) / 2 )); auto nextTarget = std::min(nextSnapshotTarget, currentTimeNewBoard + EmuDuration::sec(1)); newBoard->fastForward(nextTarget, true); auto now = Timer::getTime(); if (((now - lastProgress) > 1000000) || ((currentTimeNewBoard >= preTarget) && everShowedProgress)) { everShowedProgress = true; lastProgress = now; auto percentage = ((currentTimeNewBoard - startMSXTime) * 100u) / (preTarget - startMSXTime); reportProgress(newBoard->getReactor(), targetTime, percentage); } // note: fastForward does not always stop at // _exactly_ the requested time if (currentTimeNewBoard >= preTarget) break; if (currentTimeNewBoard >= nextSnapshotTarget) { // NOTE: there used to be //newBoard->getReactor().getEventDistributor().deliverEvents(); // here, but that has all kinds of nasty side effects: it enables // processing of hotkeys, which can cause things like the machine // being deleted, causing a crash. TODO: find a better way to support // live updates of the UI whilst being in a reverse action... newBoard->getReverseManager().takeSnapshot(currentTimeNewBoard); lastSnapshotTarget = nextSnapshotTarget; } } // re-enable automatic snapshots schedule(getCurrentTime()); // switch to the new MSXMotherBoard // Note: this deletes the current MSXMotherBoard and // ReverseManager. So we can't access those objects anymore. bool unmute = true; if (newBoard_) { unmute = false; reactor.replaceBoard(motherBoard, std::move(newBoard_)); } // Fast forward to actual target time with board activated. // This makes sure the video output gets rendered. newBoard->fastForward(targetTime, false); // In case we didn't actually create a new board, don't leave // the (old) board muted. if (unmute) { mixer.unmute(); } //assert(!isCollecting()); // can't access 'this->' members anymore! assert(newBoard->getReverseManager().isCollecting()); } catch (MSXException&) { // Make sure mixer doesn't stay muted in case of error. mixer.unmute(); throw; } } void ReverseManager::transferState(MSXMotherBoard& newBoard) { // Transfer view only mode const auto& oldDistributor = motherBoard.getStateChangeDistributor(); auto& newDistributor = newBoard .getStateChangeDistributor(); newDistributor.setViewOnlyMode(oldDistributor.isViewOnlyMode()); // transfer keyboard state auto& newManager = newBoard.getReverseManager(); if (newManager.keyboard && keyboard) { newManager.keyboard->transferHostKeyMatrix(*keyboard); } // transfer watchpoints newBoard.getDebugger().transfer(motherBoard.getDebugger()); // copy rerecord count newManager.reRecordCount = reRecordCount; // transfer settings const auto& oldController = motherBoard.getMSXCommandController(); newBoard.getMSXCommandController().transferSettings(oldController); } void ReverseManager::saveReplay( Interpreter& interp, std::span tokens, TclObject& result) { const auto& chunks = history.chunks; if (chunks.empty()) { throw CommandException("No recording..."); } std::string_view filenameArg; int maxNofExtraSnapshots = MAX_NOF_SNAPSHOTS; std::array info = {valueArg("-maxnofextrasnapshots", maxNofExtraSnapshots)}; auto args = parseTclArgs(interp, tokens.subspan(2), info); switch (args.size()) { case 0: break; // nothing case 1: filenameArg = args[0].getString(); break; default: throw SyntaxError(); } if (maxNofExtraSnapshots < 0) { throw CommandException("Maximum number of snapshots should be at least 0"); } auto filename = FileOperations::parseCommandFileArgument( filenameArg, REPLAY_DIR, "openmsx", ".omr"); auto& reactor = motherBoard.getReactor(); Replay replay(reactor); replay.reRecordCount = reRecordCount; // store current time (possibly somewhere in the middle of the timeline) // so that on load we can go back there replay.currentTime = getCurrentTime(); // restore first snapshot to be able to serialize it to a file auto initialBoard = reactor.createEmptyMotherBoard(); MemInputArchive in(begin(chunks)->second.savestate.data(), begin(chunks)->second.size, begin(chunks)->second.deltaBlocks); in.serialize("machine", *initialBoard); replay.motherBoards.push_back(std::move(initialBoard)); if (maxNofExtraSnapshots > 0) { // determine which extra snapshots to put in the replay const auto& startTime = begin(chunks)->second.time; // for the end time, try to take MAX_DIST_1_BEFORE_LAST_SNAPSHOT // seconds before the normal end time so that we get an extra snapshot // at that point, which is comfortable if you want to reverse from the // last snapshot after loading the replay. const auto& lastChunkTime = rbegin(chunks)->second.time; const auto& endTime = ((startTime + MAX_DIST_1_BEFORE_LAST_SNAPSHOT) < lastChunkTime) ? lastChunkTime - MAX_DIST_1_BEFORE_LAST_SNAPSHOT : lastChunkTime; EmuDuration totalLength = endTime - startTime; EmuDuration partitionLength = totalLength.divRoundUp(maxNofExtraSnapshots); partitionLength = std::max(MIN_PARTITION_LENGTH, partitionLength); EmuTime nextPartitionEnd = startTime + partitionLength; auto it = begin(chunks); auto lastAddedIt = begin(chunks); // already added while (it != end(chunks)) { ++it; if (it == end(chunks) || (it->second.time > nextPartitionEnd)) { --it; assert(it->second.time <= nextPartitionEnd); if (it != lastAddedIt) { // this is a new one, add it to the list of snapshots Reactor::Board board = reactor.createEmptyMotherBoard(); MemInputArchive in2(it->second.savestate.data(), it->second.size, it->second.deltaBlocks); in2.serialize("machine", *board); replay.motherBoards.push_back(std::move(board)); lastAddedIt = it; } ++it; while (it != end(chunks) && it->second.time > nextPartitionEnd) { nextPartitionEnd += partitionLength; } } } assert(lastAddedIt == std::prev(end(chunks))); // last snapshot must be included } // add sentinel when there isn't one yet bool addSentinel = history.events.empty() || !dynamic_cast(history.events.back().get()); if (addSentinel) { /// make sure the replay log ends with a EndLogEvent history.events.push_back(std::make_unique( getCurrentTime())); } try { XmlOutputArchive out(filename); replay.events = &history.events; out.serialize("replay", replay); out.close(); } catch (MSXException&) { if (addSentinel) { history.events.pop_back(); } throw; } if (addSentinel) { // Is there a cleaner way to only add the sentinel in the log? // I mean avoid changing/restoring the current log. We could // make a copy and work on that, but that seems much less // efficient. history.events.pop_back(); } result = tmpStrCat("Saved replay to ", filename); } void ReverseManager::loadReplay( Interpreter& interp, std::span tokens, TclObject& result) { bool enableViewOnly = false; std::optional where; std::array info = { flagArg("-viewonly", enableViewOnly), valueArg("-goto", where), }; auto arguments = parseTclArgs(interp, tokens.subspan(2), info); if (arguments.size() != 1) throw SyntaxError(); // resolve the filename auto context = userDataFileContext(REPLAY_DIR); std::string fileNameArg(arguments[0].getString()); std::string filename; try { // Try filename as typed by user. filename = context.resolve(fileNameArg); } catch (MSXException& /*e1*/) { try { // Not found, try adding '.omr'. filename = context.resolve(tmpStrCat(fileNameArg, ".omr")); } catch (MSXException& e2) { try { // Again not found, try adding '.gz'. // (this is for backwards compatibility). filename = context.resolve(tmpStrCat(fileNameArg, ".gz")); } catch (MSXException& /*e3*/) { // Show error message that includes the default extension. throw e2; }}} // restore replay auto& reactor = motherBoard.getReactor(); Replay replay(reactor); Events events; replay.events = &events; try { XmlInputArchive in(filename); in.serialize("replay", replay); } catch (XMLException& e) { throw CommandException("Cannot load replay, bad file format: ", e.getMessage()); } catch (MSXException& e) { throw CommandException("Cannot load replay: ", e.getMessage()); } // get destination time index auto destination = EmuTime::zero(); if (!where || (*where == "begin")) { destination = EmuTime::zero(); } else if (*where == "end") { destination = EmuTime::infinity(); } else if (*where == "savetime") { destination = replay.currentTime; } else { destination += EmuDuration(where->getDouble(interp)); } // OK, we are going to be actually changing states now // now we can change the view only mode motherBoard.getStateChangeDistributor().setViewOnlyMode(enableViewOnly); assert(!replay.motherBoards.empty()); auto& newReverseManager = replay.motherBoards[0]->getReverseManager(); auto& newHistory = newReverseManager.history; if (newReverseManager.reRecordCount == 0) { // serialize Replay version >= 4 newReverseManager.reRecordCount = replay.reRecordCount; } else { // newReverseManager.reRecordCount is initialized via // call from MSXMotherBoard to setReRecordCount() } // Restore event log swap(newHistory.events, events); auto& newEvents = newHistory.events; // Restore snapshots unsigned replayIdx = 0; for (auto& m : replay.motherBoards) { ReverseChunk newChunk; newChunk.time = m->getCurrentTime(); MemOutputArchive out(newHistory.lastDeltaBlocks, newChunk.deltaBlocks, false); out.serialize("machine", *m); newChunk.savestate = out.releaseBuffer(newChunk.size); // update replayIdx // TODO: should we use <= instead?? while (replayIdx < newEvents.size() && (newEvents[replayIdx]->getTime() < newChunk.time)) { replayIdx++; } newChunk.eventCount = replayIdx; newHistory.chunks[newHistory.getNextSeqNum(newChunk.time)] = std::move(newChunk); } // Note: until this point we didn't make any changes to the current // ReverseManager/MSXMotherBoard yet reRecordCount = newReverseManager.reRecordCount; bool noVideo = false; goTo(destination, noVideo, newHistory, false); // move to different time-line result = tmpStrCat("Loaded replay from ", filename); } void ReverseManager::transferHistory(ReverseHistory& oldHistory, unsigned oldEventCount) { assert(!isCollecting()); assert(history.chunks.empty()); // 'ids' for old and new serialize blobs don't match, so cleanup old cache oldHistory.lastDeltaBlocks.clear(); // actual history transfer history.swap(oldHistory); // resume collecting (and event recording) collecting = true; schedule(getCurrentTime()); motherBoard.getStateChangeDistributor().registerRecorder(*this); // start replaying events replayIndex = oldEventCount; // replay log contains at least the EndLogEvent assert(replayIndex < history.events.size()); replayNextEvent(); } void ReverseManager::execNewSnapshot() { // During record we should take regular snapshots, and 'now' // it's been a while since the last snapshot. But 'now' can be // in the middle of a CPU instruction (1). However the CPU // emulation code cannot handle taking snapshots at arbitrary // moments in EmuTime (2)(3)(4). So instead we send out an // event that indicates we want to take a snapshot (5). // (1) Schedulables are executed at the exact requested // EmuTime, even in the middle of a Z80 instruction. // (2) The CPU code serializes all registers, current time and // various other status info, but not enough info to be // able to resume in the middle of an instruction. // (3) Only the CPU has this limitation of not being able to // take a snapshot at any EmuTime, all other devices can. // This is because in our emulation model the CPU 'drives // time forward'. It's the only device code that can be // interrupted by other emulation code (via Schedulables). // (4) In the past we had a CPU core that could execute/resume // partial instructions (search SVN history). Though it was // much more complex and it also ran slower than the // current code. // (5) Events are delivered from the Reactor code. That code // only runs when the CPU code has exited (meaning no // longer active in any stackframe). So it's executed right // after the CPU has finished the current instruction. And // that's OK, we only require regular snapshots here, they // should not be *exactly* equally far apart in time. pendingTakeSnapshot = true; eventDistributor.distributeEvent( Event::create()); } void ReverseManager::execInputEvent() { const auto& event = *history.events[replayIndex]; try { // deliver current event at current time motherBoard.getStateChangeDistributor().distributeReplay(event); } catch (MSXException&) { // can throw in case we replay a command that fails // ignore } if (!dynamic_cast(&event)) { ++replayIndex; replayNextEvent(); } else { signalStopReplay(event.getTime()); assert(!isReplaying()); } } int ReverseManager::signalEvent(const Event& event) { (void)event; assert(getType(event) == EventType::TAKE_REVERSE_SNAPSHOT); // This event is send to all MSX machines, make sure it's actually this // machine that requested the snapshot. if (pendingTakeSnapshot) { pendingTakeSnapshot = false; takeSnapshot(getCurrentTime()); // schedule creation of next snapshot schedule(getCurrentTime()); } return 0; } unsigned ReverseManager::ReverseHistory::getNextSeqNum(EmuTime::param time) const { if (chunks.empty()) { return 0; } const auto& startTime = begin(chunks)->second.time; double duration = (time - startTime).toDouble(); return narrow(lrint(duration / SNAPSHOT_PERIOD)); } void ReverseManager::takeSnapshot(EmuTime::param time) { // (possibly) drop old snapshots // TODO does snapshot pruning still happen correctly (often enough) // when going back/forward in time? unsigned seqNum = history.getNextSeqNum(time); dropOldSnapshots<25>(seqNum); // During replay we might already have a snapshot with the current // sequence number, though this snapshot does not necessarily have the // exact same EmuTime (because we don't (re)start taking snapshots at // the same moment in time). // actually create new snapshot ReverseChunk& newChunk = history.chunks[seqNum]; newChunk.deltaBlocks.clear(); MemOutputArchive out(history.lastDeltaBlocks, newChunk.deltaBlocks, true); out.serialize("machine", motherBoard); newChunk.time = time; newChunk.savestate = out.releaseBuffer(newChunk.size); newChunk.eventCount = replayIndex; } void ReverseManager::replayNextEvent() { // schedule next event at its own time assert(replayIndex < history.events.size()); syncInputEvent.setSyncPoint(history.events[replayIndex]->getTime()); } void ReverseManager::signalStopReplay(EmuTime::param time) { motherBoard.getStateChangeDistributor().stopReplay(time); // this is needed to prevent a reRecordCount increase // due to this action ending the replay reRecordCount--; } void ReverseManager::stopReplay(EmuTime::param time) noexcept { if (isReplaying()) { // if we're replaying, stop it and erase remainder of event log syncInputEvent.removeSyncPoint(); Events& events = history.events; events.erase(begin(events) + replayIndex, end(events)); // search snapshots that are newer than 'time' and erase them auto it = ranges::find_if(history.chunks, [&](auto& p) { return p.second.time > time; }); history.chunks.erase(it, end(history.chunks)); // this also means someone is changing history, record that reRecordCount++; } assert(!isReplaying()); } /* Should be called each time a new snapshot is added. * This function will erase zero or more earlier snapshots so that there are * more snapshots of recent history and less of distant history. It has the * following properties: * - the very oldest snapshot is never deleted * - it keeps the N or N+1 most recent snapshots (snapshot distance = 1) * - then it keeps N or N+1 with snapshot distance 2 * - then N or N+1 with snapshot distance 4 * - ... and so on * @param count The index of the just added (or about to be added) element. * First element should have index 1. */ template void ReverseManager::dropOldSnapshots(unsigned count) { unsigned y = (count + N) ^ (count + N + 1); unsigned d = N; unsigned d2 = 2 * N + 1; while (true) { y >>= 1; if ((y == 0) || (count < d)) return; history.chunks.erase(count - d); d += d2; d2 *= 2; } } void ReverseManager::schedule(EmuTime::param time) { syncNewSnapshot.setSyncPoint(time + EmuDuration(SNAPSHOT_PERIOD)); } // class ReverseCmd ReverseManager::ReverseCmd::ReverseCmd(CommandController& controller) : Command(controller, "reverse") { } void ReverseManager::ReverseCmd::execute(std::span tokens, TclObject& result) { checkNumArgs(tokens, AtLeast{2}, "subcommand ?arg ...?"); auto& manager = OUTER(ReverseManager, reverseCmd); auto& interp = getInterpreter(); executeSubCommand(tokens[1].getString(), "start", [&]{ manager.start(); }, "stop", [&]{ manager.stop(); }, "status", [&]{ manager.status(result); }, "debug", [&]{ manager.debugInfo(result); }, "goback", [&]{ manager.goBack(tokens); }, "goto", [&]{ manager.goTo(tokens); }, "savereplay", [&]{ manager.saveReplay(interp, tokens, result); }, "loadreplay", [&]{ manager.loadReplay(interp, tokens, result); }, "viewonlymode", [&]{ auto& distributor = manager.motherBoard.getStateChangeDistributor(); switch (tokens.size()) { case 2: result = distributor.isViewOnlyMode(); break; case 3: distributor.setViewOnlyMode(tokens[2].getBoolean(interp)); break; default: throw SyntaxError(); }}, "truncatereplay", [&] { if (manager.isReplaying()) { manager.signalStopReplay(manager.getCurrentTime()); }}); } std::string ReverseManager::ReverseCmd::help(std::span /*tokens*/) const { return "start start collecting reverse data\n" "stop stop collecting\n" "status show various status info on reverse\n" "goback go back seconds in time\n" "goto