mirror of
https://github.com/dingusdev/dingusppc.git
synced 2024-12-22 00:29:18 +00:00
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:
parent
f415a63b76
commit
f65f9b9845
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 */
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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, ¶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;
|
||||
|
24
main.cpp
24
main.cpp
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user