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.
This commit is contained in:
Mihai Parparita 2024-11-09 22:25:48 -08:00
parent f415a63b76
commit f65f9b9845
12 changed files with 162 additions and 30 deletions

View File

@ -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<int, std::function<void(Args...)>> _slots;
mutable unsigned int _current_id { 0 };
mutable bool _is_enabled { true };
};
#endif // CORE_SIGNAL_H

View File

@ -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

View File

@ -31,8 +31,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
#include <vector>
#include <mutex>
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<void()> timer_cb;
typedef std::function<void()> 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<TimerInfo>& l, const shared_ptr<TimerInfo>& r) {
bool operator()(const std::shared_ptr<TimerInfo>& l, const std::shared_ptr<TimerInfo>& 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<uint64_t()> &cb) {
void set_time_now_cb(const std::function<uint64_t()> &cb) {
this->get_time_now = cb;
};
@ -142,10 +140,10 @@ private:
TimerManager(){}; // private constructor to implement a singleton
// timer queue
my_priority_queue<shared_ptr<TimerInfo>, vector<shared_ptr<TimerInfo>>, MyGtComparator> timer_queue;
my_priority_queue<std::shared_ptr<TimerInfo>, std::vector<std::shared_ptr<TimerInfo>>, MyGtComparator> timer_queue;
function<uint64_t()> get_time_now;
function<void()> notify_timer_changes;
std::function<uint64_t()> get_time_now;
std::function<void()> notify_timer_changes;
std::atomic<uint32_t> 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.

View File

@ -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;

View File

@ -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;

View File

@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <cpu/ppc/ppcemu.h>
#include <devices/common/hwcomponent.h>
#include <devices/common/nvram.h>
#include <devices/deviceregistry.h>
@ -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 */

View File

@ -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<std::chrono::system_clock> 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<std::chrono::seconds>(elapsed_systemclock);
return uint32_t(elapsed_seconds.count());

View File

@ -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;

View File

@ -38,6 +38,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
#include <memory>
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();

View File

@ -19,30 +19,40 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef NOMINMAX
#define NOMINMAX
#endif // NOMINMAX
#include <core/timermanager.h>
#include <cpu/ppc/ppcemu.h>
#include <devices/common/dmacore.h>
#include <devices/sound/soundserver.h>
#include <endianswap.h>
#include <algorithm>
#include <functional>
#include <loguru.hpp>
#include <cubeb/cubeb.h>
#ifdef _WIN32
#include <objbase.h>
#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<void()> deterministic_poll_cb;
};
SoundServer::SoundServer(): impl(std::make_unique<Impl>())
@ -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, &params, 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;

View File

@ -25,6 +25,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
#include <core/hostevents.h>
#include <core/timermanager.h>
#include <cpu/ppc/ppcemu.h>
#include <cpu/ppc/ppcdisasm.h>
#include <debugger/debugger.h>
#include <machines/machinebase.h>
#include <machines/machinefactory.h>
@ -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();
}

View File

@ -22,10 +22,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
#include <utils/imgfile.h>
#include <fstream>
#include <sstream>
#include <memory>
extern bool is_deterministic;
class ImgFile::Impl {
public:
std::fstream stream;
std::unique_ptr<std::iostream> stream;
};
ImgFile::ImgFile(): impl(std::make_unique<Impl>())
@ -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::stringstream>();
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<std::fstream>(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();
}