From f65f9b9845ddf0a4050213cc36ef43c9c852a6c7 Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Sat, 9 Nov 2024 22:25:48 -0800 Subject: [PATCH] Add a deterministic execution mode Adds support for a --deterministic command-line option that makes repeated runs the same: - Keyboard and mouse input is ignored - The sound server does a periodic pull from the DMA channel (so that it gets drained), but only does so via a periodic timer (instead of being driven by a cubeb callback, which could arrive at different times) - Disk image writes are disabled (reads of a modified area still work via an in-memory copy) - NVRAM writes are disabled - The current time that ViaCuda initializes the guest OS is always the same. This makes execution exactly the same each time, which should make debugging of more subtle issues easier. To validate that the deterministic mode is working, I've added a periodic log of the current "time" (measured in cycle count), PC and opcode. When comparing two runs with --log-no-uptime, the generated log files are identical. --- core/coresignal.h | 16 ++++++++ core/hostevents.h | 5 +++ core/timermanager.h | 14 +++---- cpu/ppc/ppcemu.h | 3 ++ cpu/ppc/ppcexec.cpp | 2 + devices/common/nvram.cpp | 5 +++ devices/common/viacuda.cpp | 17 ++++++++- devices/sound/awacs.cpp | 3 +- devices/sound/soundserver.h | 4 +- devices/sound/soundserver_cubeb.cpp | 59 ++++++++++++++++++++++++++--- main.cpp | 24 ++++++++++++ utils/imgfile_sdl.cpp | 40 +++++++++++++------ 12 files changed, 162 insertions(+), 30 deletions(-) diff --git a/core/coresignal.h b/core/coresignal.h index 198180f..78a1b3d 100644 --- a/core/coresignal.h +++ b/core/coresignal.h @@ -53,6 +53,9 @@ public: // Calls all connected slots. void emit(Args... args) { + if (!_is_enabled) { + return; + } for (auto const& it : _slots) { it.second(args...); } @@ -66,9 +69,22 @@ public: _slots.clear(); } + void disable() { + _is_enabled = false; + } + + void enable() { + _is_enabled = true; + } + + bool is_enabled() { + return _is_enabled; + } + private: mutable std::map> _slots; mutable unsigned int _current_id { 0 }; + mutable bool _is_enabled { true }; }; #endif // CORE_SIGNAL_H diff --git a/core/hostevents.h b/core/hostevents.h index 03e6134..cd149d5 100644 --- a/core/hostevents.h +++ b/core/hostevents.h @@ -146,6 +146,11 @@ public: _post_signal.disconnect_all(); } + void disable_input_handlers() { + _mouse_signal.disable(); + _keyboard_signal.disable(); + } + private: static EventManager* event_manager; EventManager() {}; // private constructor to implement a singleton diff --git a/core/timermanager.h b/core/timermanager.h index 465742e..51a37c3 100644 --- a/core/timermanager.h +++ b/core/timermanager.h @@ -31,8 +31,6 @@ along with this program. If not, see . #include #include -using namespace std; - #define NS_PER_SEC 1000000000 #define USEC_PER_SEC 1000000 #define NS_PER_USEC 1000 @@ -42,7 +40,7 @@ using namespace std; #define USECS_TO_NSECS(us) (us) * NS_PER_USEC #define MSECS_TO_NSECS(ms) (ms) * NS_PER_MSEC -typedef function timer_cb; +typedef std::function timer_cb; /** Extend std::priority_queue as suggested here: https://stackoverflow.com/a/36711682 @@ -101,7 +99,7 @@ typedef struct TimerInfo { // Custom comparator for sorting our timer queue in ascending order class MyGtComparator { public: - bool operator()(const shared_ptr& l, const shared_ptr& r) { + bool operator()(const std::shared_ptr& l, const std::shared_ptr& r) { return l.get()->timeout_ns > r.get()->timeout_ns; } }; @@ -116,7 +114,7 @@ public: }; // callback for retrieving current time - void set_time_now_cb(const function &cb) { + void set_time_now_cb(const std::function &cb) { this->get_time_now = cb; }; @@ -142,10 +140,10 @@ private: TimerManager(){}; // private constructor to implement a singleton // timer queue - my_priority_queue, vector>, MyGtComparator> timer_queue; + my_priority_queue, std::vector>, MyGtComparator> timer_queue; - function get_time_now; - function notify_timer_changes; + std::function get_time_now; + std::function notify_timer_changes; std::atomic id{0}; bool cb_active = false; // true if a timer callback is executing // FIXME: Do we need this? It gets written in main thread and read in audio thread. diff --git a/cpu/ppc/ppcemu.h b/cpu/ppc/ppcemu.h index 70d3f93..250913d 100644 --- a/cpu/ppc/ppcemu.h +++ b/cpu/ppc/ppcemu.h @@ -328,6 +328,9 @@ extern bool is_601; // For PowerPC 601 Emulation extern bool is_altivec; // For Altivec Emulation extern bool is_64bit; // For PowerPC G5 Emulation +// Make execution deterministic (ignore external input, used a fixed date, etc.) +extern bool is_deterministic; + // Important Addressing Integers extern uint32_t ppc_cur_instruction; extern uint32_t ppc_next_instruction_address; diff --git a/cpu/ppc/ppcexec.cpp b/cpu/ppc/ppcexec.cpp index e0fb6b2..8d8a3c8 100644 --- a/cpu/ppc/ppcexec.cpp +++ b/cpu/ppc/ppcexec.cpp @@ -57,6 +57,8 @@ MemCtrlBase* mem_ctrl_instance = 0; bool is_601 = false; +bool is_deterministic = false; + bool power_on = false; Po_Cause power_off_reason = po_enter_debugger; diff --git a/devices/common/nvram.cpp b/devices/common/nvram.cpp index 455ef0c..db21763 100644 --- a/devices/common/nvram.cpp +++ b/devices/common/nvram.cpp @@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +#include #include #include #include @@ -81,6 +82,10 @@ void NVram::init() { } void NVram::save() { + if (is_deterministic) { + LOG_F(INFO, "Skipping NVRAM write to \"%s\" in deterministic mode", this->file_name.c_str()); + return; + } ofstream f(this->file_name, ios::out | ios::binary); /* write file identification */ diff --git a/devices/common/viacuda.cpp b/devices/common/viacuda.cpp index 807b3c3..33467ee 100644 --- a/devices/common/viacuda.cpp +++ b/devices/common/viacuda.cpp @@ -751,7 +751,22 @@ void ViaCuda::pseudo_command() { } uint32_t ViaCuda::calc_real_time() { - auto end = std::chrono::system_clock::now(); + std::chrono::time_point end; + if (is_deterministic) { + // March 24, 2001 was the public release date of Mac OS X. + std::tm tm = { + .tm_sec = 0, + .tm_min = 0, + .tm_hour = 12, + .tm_mday = 24, + .tm_mon = 3 - 1, + .tm_year = 2001 - 1900, + .tm_isdst = 0 + }; + end = std::chrono::system_clock::from_time_t(std::mktime(&tm)); + } else { + end = std::chrono::system_clock::now(); + } auto elapsed_systemclock = end - this->mac_epoch; auto elapsed_seconds = std::chrono::duration_cast(elapsed_systemclock); return uint32_t(elapsed_seconds.count()); diff --git a/devices/sound/awacs.cpp b/devices/sound/awacs.cpp index 863f10c..2f8449c 100644 --- a/devices/sound/awacs.cpp +++ b/devices/sound/awacs.cpp @@ -69,8 +69,7 @@ void AwacsBase::dma_out_start() { } if (!this->out_stream_ready) { - if ((err = this->snd_server->open_out_stream(this->cur_sample_rate, - (void *)this->dma_out_ch))) { + if ((err = this->snd_server->open_out_stream(this->cur_sample_rate, this->dma_out_ch))) { LOG_F(ERROR, "%s: unable to open sound output stream: %d", this->name.c_str(), err); return; diff --git a/devices/sound/soundserver.h b/devices/sound/soundserver.h index cd74e99..8950f26 100644 --- a/devices/sound/soundserver.h +++ b/devices/sound/soundserver.h @@ -38,6 +38,8 @@ along with this program. If not, see . #include +class DmaOutChannel; + class SoundServer : public HWComponent { public: SoundServer(); @@ -45,7 +47,7 @@ public: int start(); void shutdown(); - int open_out_stream(uint32_t sample_rate, void *user_data); + int open_out_stream(uint32_t sample_rate, DmaOutChannel *dma_ch); int start_out_stream(); void close_out_stream(); diff --git a/devices/sound/soundserver_cubeb.cpp b/devices/sound/soundserver_cubeb.cpp index 9831e68..f70bf0e 100644 --- a/devices/sound/soundserver_cubeb.cpp +++ b/devices/sound/soundserver_cubeb.cpp @@ -19,30 +19,40 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX + +#include +#include #include #include #include +#include +#include #include #include #ifdef _WIN32 #include #endif -enum { +typedef enum { SND_SERVER_DOWN = 0, SND_API_READY, SND_SERVER_UP, SND_STREAM_OPENED, SND_STREAM_CLOSED -}; +} Status; class SoundServer::Impl { public: - int status; /* server status */ + Status status = SND_SERVER_DOWN; cubeb *cubeb_ctx; - cubeb_stream *out_stream; + + uint32_t deterministic_poll_timer = 0; + std::function deterministic_poll_cb; }; SoundServer::SoundServer(): impl(std::make_unique()) @@ -91,6 +101,10 @@ void SoundServer::shutdown() /* fall through */ case SND_API_READY: cubeb_destroy(impl->cubeb_ctx); + break; + case SND_SERVER_DOWN: + // Nothing to do. + break; } impl->status = SND_SERVER_DOWN; @@ -148,8 +162,30 @@ static void status_callback(cubeb_stream *stream, void *user_data, cubeb_state s LOG_F(9, "Cubeb status callback fired, status = %d", state); } -int SoundServer::open_out_stream(uint32_t sample_rate, void *user_data) +int SoundServer::open_out_stream(uint32_t sample_rate, DmaOutChannel *dma_ch) { + if (is_deterministic) { + impl->deterministic_poll_cb = [dma_ch] { + if (!dma_ch->is_out_active()) { + return; + } + // Drain the DMA buffer, but don't do anything else. + int req_size = std::max(dma_ch->get_pull_data_remaining(), 1024); + int out_size = 0; + while (req_size > 0) { + uint8_t *chunk; + uint32_t chunk_size; + if (!dma_ch->pull_data(req_size, &chunk_size, &chunk)) { + req_size -= chunk_size; + } else { + break; + } + } + }; + impl->status = SND_STREAM_OPENED; + LOG_F(9, "Deterministic sound output callback set up."); + return 0; + } int res; uint32_t latency_frames; cubeb_stream_params params; @@ -170,7 +206,7 @@ int SoundServer::open_out_stream(uint32_t sample_rate, void *user_data) res = cubeb_stream_init(impl->cubeb_ctx, &impl->out_stream, "SndOut stream", NULL, NULL, NULL, ¶ms, latency_frames, - sound_out_callback, status_callback, user_data); + sound_out_callback, status_callback, dma_ch); if (res != CUBEB_OK) { LOG_F(ERROR, "Could not open sound output stream, error: %d", res); return -1; @@ -185,11 +221,22 @@ int SoundServer::open_out_stream(uint32_t sample_rate, void *user_data) int SoundServer::start_out_stream() { + if (is_deterministic) { + LOG_F(9, "Starting sound output deterministic polling."); + impl->deterministic_poll_timer = TimerManager::get_instance()->add_cyclic_timer(MSECS_TO_NSECS(10), impl->deterministic_poll_cb); + return 0; + } return cubeb_stream_start(impl->out_stream); } void SoundServer::close_out_stream() { + if (is_deterministic) { + LOG_F(9, "Stopping sound output deterministic polling."); + TimerManager::get_instance()->cancel_timer(impl->deterministic_poll_timer); + impl->status = SND_STREAM_CLOSED; + return; + } cubeb_stream_stop(impl->out_stream); cubeb_stream_destroy(impl->out_stream); impl->status = SND_STREAM_CLOSED; diff --git a/main.cpp b/main.cpp index 4e67e49..382d75c 100644 --- a/main.cpp +++ b/main.cpp @@ -25,6 +25,7 @@ along with this program. If not, see . #include #include #include +#include #include #include #include @@ -82,6 +83,8 @@ int main(int argc, char** argv) { "Enter the built-in debugger"); app.add_option("-b,--bootrom", bootrom_path, "Specifies BootROM path") ->check(CLI::ExistingFile); + app.add_flag("--deterministic", is_deterministic, + "Make execution deterministic"); bool log_to_stderr = false; loguru::Verbosity log_verbosity = loguru::Verbosity_INFO; @@ -177,6 +180,9 @@ int main(int argc, char** argv) { cout << "BootROM path: " << bootrom_path << endl; cout << "Execution mode: " << execution_mode << endl; + if (is_deterministic) { + cout << "Using deterministic execution mode, input will be ignored." << endl; + } if (!init()) { LOG_F(ERROR, "Cannot initialize"); @@ -233,6 +239,21 @@ void run_machine(std::string machine_str, std::string bootrom_path, uint32_t exe return; } + uint32_t deterministic_timer; + if (is_deterministic) { + EventManager::get_instance()->disable_input_handlers(); + // Log the PC and instruction every second to make it easier to validate + // that execution is the same every time. + deterministic_timer = TimerManager::get_instance()->add_cyclic_timer(MSECS_TO_NSECS(100), [] { + PPCDisasmContext ctx; + ctx.instr_code = ppc_cur_instruction; + ctx.instr_addr = 0; + ctx.simplified = false; + auto op_name = disassemble_single(&ctx); + LOG_F(INFO, "TS=%016llu PC=0x%08x executing %s", get_virt_time_ns(), ppc_state.pc, op_name.c_str()); + }); + } + // set up system wide event polling using // default Macintosh polling rate of 11 ms uint32_t event_timer = TimerManager::get_instance()->add_cyclic_timer(MSECS_TO_NSECS(11), [] { @@ -273,6 +294,9 @@ void run_machine(std::string machine_str, std::string bootrom_path, uint32_t exe TimerManager::get_instance()->cancel_timer(profiling_timer); } #endif + if (is_deterministic) { + TimerManager::get_instance()->cancel_timer(deterministic_timer); + } EventManager::get_instance()->disconnect_handlers(); delete gMachineObj.release(); } diff --git a/utils/imgfile_sdl.cpp b/utils/imgfile_sdl.cpp index 30d6b5f..a5d80c8 100644 --- a/utils/imgfile_sdl.cpp +++ b/utils/imgfile_sdl.cpp @@ -22,10 +22,14 @@ along with this program. If not, see . #include #include +#include +#include + +extern bool is_deterministic; class ImgFile::Impl { public: - std::fstream stream; + std::unique_ptr stream; }; ImgFile::ImgFile(): impl(std::make_unique()) @@ -37,31 +41,43 @@ ImgFile::~ImgFile() = default; bool ImgFile::open(const std::string &img_path) { - impl->stream.open(img_path, std::ios::in | std::ios::out | std::ios::binary); - return !impl->stream.fail(); + if (is_deterministic) { + // Avoid writes to the underlying file by reading it all in memory and + // only operating on that. + auto mem_stream = std::make_unique(); + std::ifstream temp(img_path, std::ios::in | std::ios::binary); + if (!temp) return false; + *mem_stream << temp.rdbuf(); + impl->stream = std::move(mem_stream); + } else { + auto file_stream = std::make_unique(img_path, std::ios::in | std::ios::out | std::ios::binary); + if (!file_stream->is_open()) return false; + impl->stream = std::move(file_stream); + } + return impl->stream && impl->stream->good(); } void ImgFile::close() { - impl->stream.close(); + impl->stream.reset(); } uint64_t ImgFile::size() const { - impl->stream.seekg(0, impl->stream.end); - return impl->stream.tellg(); + impl->stream->seekg(0, impl->stream->end); + return impl->stream->tellg(); } uint64_t ImgFile::read(void* buf, uint64_t offset, uint64_t length) const { - impl->stream.seekg(offset, std::ios::beg); - impl->stream.read((char *)buf, length); - return impl->stream.gcount(); + impl->stream->seekg(offset, std::ios::beg); + impl->stream->read((char *)buf, length); + return impl->stream->gcount(); } uint64_t ImgFile::write(const void* buf, uint64_t offset, uint64_t length) { - impl->stream.seekg(offset, std::ios::beg); - impl->stream.write((const char *)buf, length); - return impl->stream.gcount(); + impl->stream->seekg(offset, std::ios::beg); + impl->stream->write((const char *)buf, length); + return impl->stream->gcount(); }