/* DingusPPC - The Experimental PowerPC Macintosh emulator Copyright (C) 2018-26 The DingusPPC Development Team (See CREDITS.MD for more details) (You may also contact divingkxt or powermax2286 on Discord) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // The main runfile - main.cpp // This is where the magic begins #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #else #include #endif using namespace std; extern bool g_auto_grab_mouse; extern bool g_swap_command_option; static void sigint_handler(int signum) { power_off(po_signal_interrupt); } static void sigabrt_handler(int signum) { LOG_F(INFO, "Shutting down..."); delete gMachineObj.release(); cleanup(); } static string appDescription = string( "\nDingusPPC - Alpha 1.04c (10/4/2026) " "\nWritten by divingkatae, maximumspatium, " "\njoevt, mihaip, kkaisershot, et. al. " "\n(c) 2018-2026 The DingusPPC Dev Team. " "\nThis is a build intended for testing. " "\nUse at your own discretion. " "\n" ); static uint32_t keyboard_id = 0; static std::optional parse_calendar_date(const std::string& value) { if (value.size() != 10 || value[4] != '-' || value[7] != '-') return std::nullopt; for (size_t i = 0; i < value.size(); i++) { if (i != 4 && i != 7 && (value[i] < '0' || value[i] > '9')) return std::nullopt; } std::chrono::year_month_day date{ std::chrono::year{std::stoi(value.substr(0, 4))}, std::chrono::month{unsigned(std::stoi(value.substr(5, 2)))}, std::chrono::day{unsigned(std::stoi(value.substr(8, 2)))} }; if (!date.ok() || int(date.year()) == 0) return std::nullopt; return date; } static std::optional parse_start_date(std::string value, bool is_deterministic) { if (value.empty()) { if (!is_deterministic) return std::nullopt; // March 24, 2001 was the public release date of Mac OS X. value = "2001-03-24"; } auto date = *parse_calendar_date(value); int seconds_of_day = 12 * 60 * 60; // Noon if (!is_deterministic) { auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); auto local_time = *std::localtime(&now); seconds_of_day = local_time.tm_hour * 60 * 60 + local_time.tm_min * 60 + local_time.tm_sec; } return std::chrono::local_days{date} + std::chrono::seconds{seconds_of_day}; } /// Check for an existing directory (returns error message if check fails) class WorkingDirectoryValidator : public CLI::detail::ExistingDirectoryValidator { public: WorkingDirectoryValidator() { func_ = [](std::string& filename) { std::string result = CLI::ExistingDirectory.operator()(filename); if (result.empty()) { #ifdef _WIN32 _chdir(filename.c_str()); #else chdir(filename.c_str()); #endif } return result; }; } }; const WorkingDirectoryValidator WorkingDirectory; void run_machine( std::string machine_str, char *rom_data, size_t rom_size, uint32_t execution_mode ,const std::vector &env_vars ,bool deterministic_interactive ,uint32_t profiling_interval_ms ); int main(int argc, char** argv) { uint32_t execution_mode = interpreter; CLI::App app(appDescription); app.allow_extras(); app.set_help_all_flag("--help-all", "Print this help message, help for subcommands, and exit"); bool debugger_skip = true; bool debugger_enter = false; bool deterministic_interactive = false; string deterministic_mode = "strict"; string start_date; string keyboard_string = "Eng_USA"; const std::map kbd_map{ {"Eng_USA", 0}, {"Eng_GBR", 1}, {"Fra_FRA", 10}, {"Deu_DEU", 20}, {"Ita_ITA", 30},{"Spa_ESP", 40}, {"Jpn_JPN", 80}, }; string bootrom_path("bootrom.bin"); string working_directory_path("."); auto emu = app.add_subcommand("", "Emulation"); auto execution_mode_group = emu->add_option_group("execution mode") ->require_option(-1); execution_mode_group->add_flag("-r,--run", debugger_skip, "Run the emulator immediately (skip enterring the built-in debugger)"); execution_mode_group->add_flag("-d,--debugger", debugger_enter, "Enter the built-in debugger"); emu->add_option("-k,--keyboard", keyboard_string, "Specify keyboard ID"); emu->add_option("-w,--workingdir", working_directory_path, "Specifies working directory") ->check(WorkingDirectory)->capture_default_str(); auto bootrom_opt = emu->add_option("-b,--bootrom", bootrom_path, "Specifies BootROM path") ->check(CLI::ExistingFile)->capture_default_str(); auto deterministic_opt = emu->add_flag("--deterministic", is_deterministic, "Use deterministic execution"); emu->add_option("--start-date", start_date, "Set the guest RTC start date (YYYY-MM-DD), keeping the current local time; " "deterministic mode defaults to 2001-03-24 at noon") ->check(CLI::Validator([](std::string& value) { return parse_calendar_date(value) ? std::string() : std::string("Expected a valid date in YYYY-MM-DD format"); }, "YYYY-MM-DD")); emu->add_option("--deterministic-mode", deterministic_mode, "Select deterministic features (strict or interactive)") ->needs(deterministic_opt) ->check(CLI::IsMember({"strict", "interactive"})); bool log_to_stderr = false; loguru::Verbosity log_verbosity = loguru::Verbosity_INFO; bool log_no_uptime = false; emu->add_flag("--log-to-stderr", log_to_stderr, "Send internal logging to stderr (instead of dingusppc.log)"); emu->add_option("--log-verbosity", log_verbosity, "Adjust logging verbosity (default is 0 a.k.a. INFO)") ->check(CLI::Number); emu->add_flag("--log-no-uptime", log_no_uptime, "Disable the uptime preamble of logged messages"); std::vector env_vars; emu->add_option("--setenv", env_vars, "Set Open Firmware variables at startup") ->take_all(); uint32_t profiling_interval_ms = 0; #ifdef CPU_PROFILING emu->add_option("--profiling-interval-ms", profiling_interval_ms, "Specifies periodic interval (in ms) at which to output CPU profiling information"); #endif string machine_str; CLI::Option* machine_opt = emu->add_option("-m,--machine", machine_str, "Specify machine ID"); emu->add_flag("--auto-grab-mouse", g_auto_grab_mouse, "The guest cursor causes mouse to be grabbed"); emu->add_flag("--swap-command-option", g_swap_command_option, "Swap the Command and Option keys (physical Alt/AltGr becomes Command)"); auto list_cmd = app.add_subcommand("list", "Display available machine configurations and exit"); string sub_arg; list_cmd->add_option("machines", sub_arg, "List supported machines"); list_cmd->add_option("properties", sub_arg, "List available properties"); CLI11_PARSE(app, argc, argv); deterministic_interactive = deterministic_mode == "interactive"; if (*list_cmd) { if (sub_arg == "machines") { MachineFactory::list_machines(); } else if (sub_arg == "properties") { MachineFactory::list_properties(); } else { cout << "Unknown list subcommand " << sub_arg << endl; } return 0; } if (bootrom_opt->count() == 0) { // it was not specified on the command line, so validate the default file name. std::string msg = bootrom_opt->get_validator()->operator()(bootrom_path); if (!msg.empty()) { CLI::ParseError e(msg, CLI::ExitCodes::ValidationError); return app.exit(e); } } if (debugger_enter || !debugger_skip) { execution_mode = debugger; } if (auto start_time = parse_start_date(start_date, is_deterministic)) ViaCuda::set_start_time(*start_time); /* initialize logging */ loguru::g_preamble_date = false; loguru::g_preamble_time = false; loguru::g_preamble_thread = false; loguru::g_preamble_uptime = !log_no_uptime; if (execution_mode == interpreter && !log_to_stderr) { loguru::g_stderr_verbosity = loguru::Verbosity_OFF; loguru::init(argc, argv); loguru::add_file("dingusppc.log", loguru::Append, log_verbosity); } else { loguru::g_stderr_verbosity = log_verbosity; loguru::init(argc, argv); } auto rom_data = std::unique_ptr(new char[4 * 1024 * 1024]); memset(&rom_data[0], 0, static_cast(4 * 1024 * 1024)); size_t rom_size = MachineFactory::read_boot_rom(bootrom_path, &rom_data[0]); if (!rom_size) { return 1; } string machine_str_from_rom = MachineFactory::machine_name_from_rom(&rom_data[0], rom_size); if (machine_str_from_rom.empty()) { LOG_F(ERROR, "Could not autodetect machine from ROM."); } else { LOG_F(INFO, "Machine detected from ROM as: %s", machine_str_from_rom.c_str()); } if (*machine_opt) { LOG_F(INFO, "Machine option was passed in: %s", machine_str.c_str()); } else { machine_str = machine_str_from_rom; } if (machine_str.empty()) { LOG_F(ERROR, "Must specify a machine or provide a supported ROM."); return 1; } // Hook to allow properties to be read from the command-line, regardless // of when they are registered. MachineFactory::get_setting_value = [&](const std::string& name) -> std::optional { CLI::App sa; sa.allow_extras(); std::string value; sa.add_option("--" + name, value)->expected(0,1); try { sa.parse(app.remaining_for_passthrough()); } catch (const CLI::Error& e) { ABORT_F("Cannot parse CLI: %s", e.get_name().c_str()); } if (sa.count("--" + name) > 0) { return value; } else { return std::nullopt; } }; if (MachineFactory::register_machine_settings(machine_str) < 0) { return 1; } cout << "BootROM path: " << bootrom_path << endl; cout << "Execution mode: " << execution_mode << endl; if (is_deterministic) { cout << "Using deterministic execution mode; disk, NVRAM, and PRAM changes will not be saved." << endl; if (deterministic_interactive) { cout << "Mouse and keyboard input enabled; execution will not be fully deterministic." << endl; } else { cout << "Mouse and keyboard input will be ignored." << endl; } } if (!init()) { LOG_F(ERROR, "Cannot initialize"); return 1; } // initialize global profiler object gProfilerObj.reset(new Profiler()); // graceful handling of fatal errors loguru::set_fatal_handler([](const loguru::Message& message) { // Make sure the reason for the failure is visible (it may have been // sent to the logfile only). cerr << message.preamble << message.indentation << message.prefix << message.message << endl; set_power_off_reason(po_enter_debugger); DppcDebugger::get_instance()->enter_debugger(); // Ensure that NVRAM and other state is persisted before we terminate. delete gMachineObj.release(); }); // redirect SIGINT to our own handler signal(SIGINT, sigint_handler); // redirect SIGABRT to our own handler signal(SIGABRT, sigabrt_handler); keyboard_id = kbd_map.at(keyboard_string); while (true) { run_machine( machine_str, &rom_data[0], rom_size, execution_mode, env_vars, deterministic_interactive, profiling_interval_ms); if (power_off_reason == po_restarting) { LOG_F(INFO, "Restarting..."); power_on = true; continue; } if (power_off_reason == po_shutting_down) { if (execution_mode != debugger) { LOG_F(INFO, "Shutdown."); break; } LOG_F(INFO, "Shutdown..."); power_on = true; continue; } break; } // if we didn't delete this then delete it now delete gMachineObj.release(); cleanup(); return 0; } void run_machine(std::string machine_str, char* rom_data, size_t rom_size, uint32_t execution_mode, const std::vector &env_vars, bool deterministic_interactive, uint32_t #ifdef CPU_PROFILING profiling_interval_ms #endif ) { if (MachineFactory::create_machine_for_id(machine_str, rom_data, rom_size) < 0) { return; } if (!env_vars.empty()) { OfConfigUtils ofnvram; if (!ofnvram.init()) { for (const auto &env_var : env_vars) { auto pos = env_var.find('='); if (pos != std::string::npos) { std::string name = env_var.substr(0, pos); std::string value = env_var.substr(pos + 1); if (ofnvram.setenv(name, value)) { LOG_F(INFO, "Set Open Firmware variable %s to %s", name.c_str(), value.c_str()); } else { LOG_F(WARNING, "Cannot set Open Firmware variable %s to %s", name.c_str(), value.c_str()); } } else { LOG_F(WARNING, "Invalid format for --setenv: %s", env_var.c_str()); } } } else { LOG_F(WARNING, "Cannot initialize NVRAM wrapper, will not set Open Firmware variables"); } } static TimerInfo deterministic_timer; if (is_deterministic && !deterministic_interactive) { 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. TimerManager::get_instance()->add_cyclic_timer(deterministic_timer, MSECS_TO_NSECS(1000), [](uint64_t, uint64_t) { // We may be running after an exceptions or RFI changed the address // translation context. In those cases the PC address cannot be // translated (it would either be stale or invalid). if (exec_flags & (EXEF_EXCEPTION | EXEF_RFI)) { LOG_F(INFO, "TS=%016llu PC=0x%08x transition pending to PC=0x%08x", get_virt_time_ns(), ppc_state.pc, ppc_next_instruction_address); return; } PPCDisasmContext ctx; ctx.instr_code = ppc_read_instruction(mmu_translate_imem(ppc_state.pc)); ctx.instr_addr = ppc_state.pc; 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()); }); } EventManager::get_instance()->set_keyboard_locale(keyboard_id); // set up system wide event polling using // default Macintosh polling rate of 11 ms static TimerInfo event_timer; TimerManager::get_instance()->add_cyclic_timer(event_timer, MSECS_TO_NSECS(11), [](uint64_t, uint64_t) { EventManager::get_instance()->poll_events(); }); #ifdef CPU_PROFILING uint32_t profiling_timer; if (profiling_interval_ms > 0) { profiling_timer = TimerManager::get_instance()->add_cyclic_timer(MSECS_TO_NSECS(profiling_interval_ms), [](uint64_t, uint64_t) { gProfilerObj->print_profile("PPC_CPU"); }); } #endif switch (execution_mode) { case interpreter: set_power_off_reason(po_starting_up); DppcDebugger::get_instance()->enter_debugger(); break; case threaded_int: set_power_off_reason(po_starting_up); DppcDebugger::get_instance()->enter_debugger(); break; case debugger: set_power_off_reason(po_enter_debugger); DppcDebugger::get_instance()->enter_debugger(); break; default: LOG_F(ERROR, "Invalid EXECUTION MODE"); return; } LOG_F(INFO, "Cleaning up..."); TimerManager::get_instance()->cancel_timer(event_timer); #ifdef CPU_PROFILING if (profiling_interval_ms > 0) { TimerManager::get_instance()->cancel_timer(profiling_timer); } #endif if (is_deterministic && !deterministic_interactive) { TimerManager::get_instance()->cancel_timer(deterministic_timer); } TimerManager::get_instance()->cancel_all_timers(); EventManager::get_instance()->disconnect_handlers(); delete gMachineObj.release(); }