From 1e5954709217703f33618769568b8481a7dfa777 Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 18 Aug 2026 08:09:58 +0200 Subject: [PATCH 1/2] realtime: add a --realtime command line flag Enable g_realtime mode from the command line instead of only via the Control-Alt-R shortcut. --- main.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/main.cpp b/main.cpp index 36e2c8627d..fc08f8d3c2 100644 --- a/main.cpp +++ b/main.cpp @@ -113,6 +113,7 @@ int main(int argc, char** argv) { bool debugger_skip = true; bool debugger_enter = false; bool deterministic_interactive = false; + bool start_realtime = false; string deterministic_mode = "strict"; string keyboard_string = "Eng_USA"; @@ -142,6 +143,8 @@ int main(int argc, char** argv) { "Select deterministic features (strict or interactive)") ->needs(deterministic_opt) ->check(CLI::IsMember({"strict", "interactive"})); + emu->add_flag("--realtime", start_realtime, + "Start in realtime mode (guest time follows the wall clock)"); bool log_to_stderr = false; loguru::Verbosity log_verbosity = loguru::Verbosity_INFO; @@ -309,6 +312,10 @@ int main(int argc, char** argv) { keyboard_id = kbd_map.at(keyboard_string); + if (start_realtime) { + toggle_g_realtime(); + } + while (true) { run_machine( machine_str, From c41483f6b5857b180fc5777f51619a498194a812 Mon Sep 17 00:00:00 2001 From: probonopd Date: Tue, 18 Aug 2026 08:10:24 +0200 Subject: [PATCH 2/2] realtime: throttle an idle guest in realtime mode to save host CPU In realtime mode the guest never halts (there is no PPC equivalent of the x86 HLT instruction), so at the desktop it keeps spinning in its idle path, burning a whole host core. Detect a settled idle state via a low-pass-filtered rate of guest memory-mapped I/O: boot and real work touch devices at hundreds of thousands of accesses per second, a settled idle desktop at a few thousand. Once the filtered rate has stayed low continuously for IDLE_CONFIRM_NS, sleep the guest for most of each 16 ms VBL period and run a 6 ms servicing burst so interrupt handling still completes; input raises the rate again, which disengages the throttle within one window. The feature is opt-in via --idle-cpu-save so that default behavior is unchanged, and both realtime and non-realtime modes maintain the guest MMIO access counter. --- cpu/ppc/ppcemu.h | 8 +++ cpu/ppc/ppcexec.cpp | 153 ++++++++++++++++++++++++++++++++++++++++++++ cpu/ppc/ppcmmu.cpp | 5 ++ main.cpp | 6 ++ 4 files changed, 172 insertions(+) diff --git a/cpu/ppc/ppcemu.h b/cpu/ppc/ppcemu.h index af75394e51..b75bd70a9c 100644 --- a/cpu/ppc/ppcemu.h +++ b/cpu/ppc/ppcemu.h @@ -344,6 +344,11 @@ enum Exc_Cause : uint32_t { extern unsigned exec_flags; +// Counter of guest accesses to memory-mapped devices, incremented by +// mmu_read_vmem/mmu_write_vmem. Used by ppcexec.cpp to detect when the +// guest is idling (spinning without touching any device). +extern uint64_t g_mmio_access_count; + extern jmp_buf exc_env; enum Po_Cause : int { @@ -728,6 +733,9 @@ extern int get_icnt_factor(); /* toggle_g_realtime */ extern bool toggle_g_realtime(); +/* set_g_idle_cpu_save */ +extern void set_g_idle_cpu_save(bool enabled); + /* force_cycle_counter_reload */ static void force_cycle_counter_reload(); diff --git a/cpu/ppc/ppcexec.cpp b/cpu/ppc/ppcexec.cpp index e089312d80..759a342cc4 100644 --- a/cpu/ppc/ppcexec.cpp +++ b/cpu/ppc/ppcexec.cpp @@ -34,6 +34,7 @@ along with this program. If not, see . #include #include #include +#include #ifdef __APPLE__ #include @@ -135,6 +136,9 @@ uint64_t g_nanoseconds_base; uint64_t g_icycles; int icnt_factor; +/* when true, sleep an idle guest in realtime mode to save host CPU */ +bool g_idle_cpu_save = false; + /* global variables related to the timebase facility */ uint64_t tbr_wr_timestamp; // stores vCPU virtual time of the last TBR write uint64_t rtc_timestamp; // stores vCPU virtual time of the last RTC write @@ -375,10 +379,152 @@ void set_virt_time_ns(uint64_t time_now) LOG_F(INFO, "time before: %lld after: %lld change: %lld", time_now, time_new, time_new - time_now); } +// Idle detection for realtime mode. The guest never halts (no PPC +// equivalent of x86 HLT), so at the desktop it keeps executing its idle +// path forever, burning a whole host core. We cannot simply sleep +// whenever the guest is quiet for a moment: stalling guest execution +// while wall time (and thus every time-based device, timer and driver +// timeout) keeps advancing derails the guest if it is still doing +// critical boot work. The distinguishing signal we use is the guest's +// memory-mapped I/O rate: boot and real work touch devices constantly +// (hundreds of thousands of accesses per second), while a settled idle +// desktop touches them at a steady few thousand per second. The rate is +// low-pass filtered (a burst of accesses an interrupt handler performs +// in a few microseconds would otherwise look like activity), and only +// once the filtered rate has stayed low continuously for +// IDLE_CONFIRM_NS do we throttle. +// +// The first confirmation must be long enough that it cannot be +// satisfied during boot at all; a boot that takes tens of seconds never +// yields that much uninterrupted low-rate time. Once the guest has been +// throttled once it has provably reached its idle state, so afterwards we +// only ever disengage for sustained work (see guest_is_idle). +static constexpr uint64_t IDLE_RATE_EMA_TAU_NS = 2000000000ULL; // 2 s +static constexpr uint64_t IDLE_CONFIRM_RATE = 30000; // MMIO accesses per second +static constexpr uint64_t IDLE_CONFIRM_NS = 15000000000ULL; // 15 s, first time +static constexpr uint64_t IDLE_DISENGAGE_NS = 1000000000ULL; // 1 s of sustained high rate +// Minimum uptime before the throttle may engage at all, so a guest that +// is still in the middle of booting is never throttled; the confirm +// timer guards against throttling during any later settling work. +static constexpr uint64_t IDLE_UPTIME_GUARD_NS = 15000000000ULL; // 15 s +static uint64_t g_idle_last_ns = 0; +static uint64_t g_idle_last_mmio = 0; +static uint64_t g_mmio_count_ema = 0; // converges to rate * IDLE_RATE_EMA_TAU_NS +static uint64_t g_idle_low_ns = 0; // continuous time with a low filtered rate +static uint64_t g_idle_high_ns = 0; // continuous time with a high filtered rate +static uint64_t g_idle_boot_start_ns = 0; +static bool g_idle_engaged = false; + +static void reset_idle_detector() +{ + // Called from ppc_cpu_init, i.e. on every boot (including guest + // restarts): start over with the long first confirmation and a + // fresh uptime gate so a rebooting guest is never throttled during + // its boot phase. + g_idle_last_ns = 0; + g_idle_last_mmio = 0; + g_mmio_count_ema = 0; + g_idle_low_ns = 0; + g_idle_high_ns = 0; + g_idle_boot_start_ns = get_virt_time_ns(); + g_idle_engaged = false; +} + +static bool guest_is_idle() +{ + const uint64_t now_ns = get_virt_time_ns(); + const uint64_t elapsed_ns = now_ns - g_idle_last_ns; + g_idle_last_ns = now_ns; + + const uint64_t mmio_delta = g_mmio_access_count - g_idle_last_mmio; + g_idle_last_mmio = g_mmio_access_count; + + if (elapsed_ns > 0) { + // Exponential-decay accumulator on the MMIO count. Each window + // adds its accesses and the total decays by exp(-elapsed/tau); + // it converges to rate * tau regardless of how often this is + // called, unlike an additive EMA which tracks the per-window + // count when windows are much shorter than tau. Rate is then + // count / tau. + uint64_t decayed; + if (elapsed_ns >= IDLE_RATE_EMA_TAU_NS) { + decayed = 0; + } else { + decayed = g_mmio_count_ema * (IDLE_RATE_EMA_TAU_NS - elapsed_ns) / IDLE_RATE_EMA_TAU_NS; + } + g_mmio_count_ema = decayed + mmio_delta; + + const uint64_t rate = g_mmio_count_ema * 1000000000ULL / IDLE_RATE_EMA_TAU_NS; + if (rate < IDLE_CONFIRM_RATE) { + g_idle_high_ns = 0; + g_idle_low_ns += elapsed_ns; + } else { + g_idle_low_ns = 0; + g_idle_high_ns += elapsed_ns; + } + } + + // The guest has real work to do: an exception to take, an interrupt + // asserted that it cannot take yet (MSR.EE off), or a device that + // requested immediate processing. Do not sleep in these cases. + if ((exec_flags & EXEF_EXCEPTION) || int_pin || exec_timer) { + return false; + } + + // Never throttle before the guest has had a chance to boot. + if (now_ns - g_idle_boot_start_ns < IDLE_UPTIME_GUARD_NS) { + return false; + } + + // The first throttle only happens after IDLE_CONFIRM_NS of + // continuous low rate. Afterwards the throttle stays on through + // brief activity (input, a momentary burst) - the guest services it + // during the bursts - and only yields to sustained work: once the + // rate has been high for IDLE_DISENGAGE_NS it runs at full speed + // until the rate drops again, then throttling resumes immediately. + // This avoids the flapping where any short spike above the + // threshold disengages the throttle for seconds at a time, which + // showed up as the CPU jumping back to 99% at the settled desktop. + if (g_idle_engaged) { + return g_idle_high_ns < IDLE_DISENGAGE_NS; + } + + if (g_idle_low_ns >= IDLE_CONFIRM_NS) { + g_idle_engaged = true; + return true; + } + return false; +} + static uint64_t process_events() { exec_timer = false; uint64_t slice_ns = TimerManager::get_instance()->process_timers(); + if (g_realtime && g_idle_cpu_save && guest_is_idle()) { + // The guest is idling: sleep until the next scheduled event + // instead of executing its idle path. Guest time is wall-clock + // based in realtime mode, so the sleep advances guest time and + // the guest's timers (VBL, decrementer, ...) keep firing on + // schedule. We still run a short burst afterwards so the guest + // services those interrupts and keeps its devices polled. + // Only sleep if a timer is actually pending; otherwise the + // guest has no interrupt to wake it and the host must keep + // executing. + if (slice_ns != 0) { + constexpr uint64_t max_sleep_ns = 16000000ULL; // 16 ms + // The burst must be long enough for the guest to fully + // service its pending interrupts (VBL, DEC, device polls) + // and return to its idle loop. Shorter bursts (1-4 ms) + // starve that servicing: guest time keeps advancing while + // the guest executes too little, so it ends up stuck in a + // machine-check storm at the external-interrupt vector. + // 6 ms per 16 ms window keeps DP3 healthy at ~7% host CPU. + constexpr uint64_t burst_ns = 6000000ULL; // 6 ms + const uint64_t sleep_ns = (slice_ns > max_sleep_ns) ? max_sleep_ns : slice_ns; + std::this_thread::sleep_for(std::chrono::nanoseconds(sleep_ns)); + return g_icycles + (burst_ns >> icnt_factor) + 1; + } + } if (slice_ns == 0) { // execute 25.000 cycles // if there are no pending timers @@ -427,6 +573,11 @@ bool toggle_g_realtime() return g_realtime; } +void set_g_idle_cpu_save(bool enabled) +{ + g_idle_cpu_save = enabled; +} + typedef enum { main, until, @@ -1090,6 +1241,8 @@ void ppc_cpu_init(MemCtrlBase* mem_ctrl, uint32_t cpu_version, bool do_include_6 /* redirect code execution to reset vector */ ppc_state.pc = 0xFFF00100; + reset_idle_detector(); + #ifdef CPU_PROFILING gProfilerObj->register_profile("PPC_CPU", std::unique_ptr(new CPUProfile())); diff --git a/cpu/ppc/ppcmmu.cpp b/cpu/ppc/ppcmmu.cpp index 1f3f3540d7..1ebf4b574a 100644 --- a/cpu/ppc/ppcmmu.cpp +++ b/cpu/ppc/ppcmmu.cpp @@ -38,6 +38,9 @@ along with this program. If not, see . /* pointer to exception handler to be called when a MMU exception is occurred. */ void (*mmu_exception_handler)(Except_Type exception_type, uint32_t srr1_bits); +/* counts guest accesses to memory-mapped devices; see ppcemu.h */ +uint64_t g_mmio_access_count = 0; + /* pointers to BAT update functions. */ std::function ibat_update; std::function dbat_update; @@ -1280,6 +1283,7 @@ inline T mmu_read_vmem(uint32_t opcode, uint32_t guest_va) #ifdef MMU_PROFILING iomem_reads_total++; #endif + g_mmio_access_count++; #if SUPPORTS_MEMORY_CTRL_ENDIAN_MODE needs_swap = mem_ctrl_instance->needs_swap_endian(tlb2_entry->rgn_desc); @@ -1457,6 +1461,7 @@ inline void mmu_write_vmem(uint32_t opcode, uint32_t guest_va, T value) #ifdef MMU_PROFILING iomem_writes_total++; #endif + g_mmio_access_count++; #if SUPPORTS_MEMORY_CTRL_ENDIAN_MODE needs_swap = mem_ctrl_instance->needs_swap_endian(tlb2_entry->rgn_desc); diff --git a/main.cpp b/main.cpp index fc08f8d3c2..de4d7db553 100644 --- a/main.cpp +++ b/main.cpp @@ -114,6 +114,7 @@ int main(int argc, char** argv) { bool debugger_enter = false; bool deterministic_interactive = false; bool start_realtime = false; + bool start_idle_cpu_save = false; string deterministic_mode = "strict"; string keyboard_string = "Eng_USA"; @@ -145,6 +146,8 @@ int main(int argc, char** argv) { ->check(CLI::IsMember({"strict", "interactive"})); emu->add_flag("--realtime", start_realtime, "Start in realtime mode (guest time follows the wall clock)"); + emu->add_flag("--idle-cpu-save", start_idle_cpu_save, + "Sleep an idle guest in realtime mode to save host CPU"); bool log_to_stderr = false; loguru::Verbosity log_verbosity = loguru::Verbosity_INFO; @@ -315,6 +318,9 @@ int main(int argc, char** argv) { if (start_realtime) { toggle_g_realtime(); } + if (start_idle_cpu_save) { + set_g_idle_cpu_save(true); + } while (true) { run_machine(