diff --git a/CMakeLists.txt b/CMakeLists.txt index eb48055c..3b9b0681 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -307,6 +307,7 @@ if (NOT WITHOUT_QEMU) qemu-components/common/src/libqemu-cxx/gpex.cc qemu-components/common/src/libqemu-cxx/gpio.cc qemu-components/common/src/libqemu-cxx/libqemu-cxx.cc + qemu-components/common/src/libqemu-cxx/libqemu-plugin.cc qemu-components/common/src/libqemu-cxx/memory.cc qemu-components/common/src/libqemu-cxx/object.cc qemu-components/common/src/libqemu-cxx/rcu-read-lock.cc @@ -362,6 +363,16 @@ if (NOT WITHOUT_QEMU) $ ) + # Glib shim path (BUILD_INTERFACE only): shadows system glib for any + # in-tree TU that transitively includes qemu-plugin.h via libqemu's + # generated typedefs.h (which sits inside an extern "C" block in + # libqemu.h). Forward-decls of GArray/GByteArray are sufficient + # because qbox C++ never accesses glib internals. + target_include_directories( + ${PROJECT_NAME} BEFORE PUBLIC + $ + ) + target_link_libraries(${PROJECT_NAME} PUBLIC libqemu) set(LIBQEMU_CXX_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/qemu-components/common/include/libqemu-cxx) diff --git a/README.md b/README.md index fbfa5908..9fa68f32 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,341 @@ cmake -B build -DCMAKE_BUILD_TYPE=Release cmake -B build -DLIBQEMU_TARGETS="aarch64;riscv64" ``` +## MCIPS Plugin (Multi-Core Instructions Per Second Plugin) + +The **MCIPS plugin** is a SystemC/C++ component that provides time synchronization between **QEMU** and **SystemC** by leveraging QEMU's TCG (Tiny Code Generator) plugin API. It enables deterministic multi-core simulation with instruction-level timing accuracy. + +> **Note:** The MCIPS plugin currently supports **TCG mode only**. + +*** + +### 1. Enabling the MCIPS Plugin from Lua Configuration + +To enable the plugin, set `enable_mcips_plugin = true` inside the `QemuInstance` configuration: + +```lua +qemu_inst = { + moduletype = "QemuInstance"; + args = {"&platform.qemu_inst_mgr", "AARCH64"}; + accel = "tcg", -- MCIPS requires TCG mode + tcg_mode = "MULTI", -- Multi-threaded TCG + sync_policy = "multithread-unconstrained", + + enable_mcips_plugin = true, -- Enable MCIPS plugin + + log_level = 0 +} +``` + +Setting this to `false` disables the MCIPS plugin and falls back to the traditional quantum keeper synchronization mechanism. + +Each CPU can be configured with its own instruction execution rate: + +```lua +for i = 0, (ARM_NUM_CPUS - 1) do + local cpu = { + moduletype = "cpu_arm_cortexA76"; + args = {"&platform.qemu_inst"}; + mem = {bind = "&router.target_socket"}; + + insn_per_second = 100000000; -- 100 MIPS per CPU + -- insn_per_second = ((i+1) * 100000000); -- Different speeds per CPU + }; + platform["cpu_"..tostring(i)] = cpu; +end +``` + +*** + +### 2. How the MCIPS Plugin Works + +The MCIPS plugin is implemented as a **SystemC module** (`McipsPlugin` in `mcips-plugin.h`) that inherits from the **`LibQemuPlugin`** base class. Here is how it operates: + +#### **Thread Model** + +| Thread | Entry points | +|---|---| +| QEMU vCPU (BQL held) | `vcpu_init`, `vcpu_idle`, `vcpu_resume`, `vcpu_tb_trans`, `vcpu_tb_exec_cond`, `cpu_end_delta_quota` | +| SystemC | `receive_window_cb`, `idle_tick_method` | +| QEMU timer | `get_qemu_clock` (no locks) | + +**Lock ordering:** `BQL → m_mcips_mutex` (never reversed). + +#### **Lock-Free Atomics** + +| Variable | Ordering | Purpose | +|---|---|---| +| `m_shutdown` | seq_cst | Shutdown flag; checked on all entry paths | +| `m_inflight_cb` | seq_cst | Shutdown drain counter for `receive_window_cb` | +| `m_qemu_time_ns` | release/acquire | Nanosecond shadow of `m_qemu_time` for `get_qemu_clock` | +| `m_active_vcpu` | acquire (lock-free) / relaxed (under mutex) | Active CPU pointer | +| `m_all_cpus_idle` | release/acquire | Idle-pump flag | + +#### **QEMU TCG Plugin Integration** +- **libidlinker Plugin**: Runs QEMU's libidlinker shared library (`libidlinker.so`/`.dylib`) +- **Plugin ID Communication**: libidlinker calls `global_set_cci_param()` to send plugin ID back to QBox; the ID is stored as a `cci::cci_param` (`m_id`) +- **Event Registration**: ID-keyed callbacks use the `REGISTER_CALLBACK_ID` macro; userdata-keyed callbacks are registered directly via `m_inst.plugin_api().qemu_plugin_register_*_cb(...)` with a static bridge that calls `dispatch_userdata()`. +- **Bridge Functions**: Static bridge functions route QEMU callbacks to C++ member functions + +#### **Instruction-Based Timing Model** +- **Instruction Quota**: Each vCPU runs a global quantum (instruction count limit) before synchronization +- **Delta Tracking**: Tracks `delta_insn` (instructions executed since last sync) per vCPU using QEMU scoreboards +- **Time Calculation**: Converts instruction count to simulation time using `insn_per_second` parameter. `qemu_time_now()` has an overload accepting the active pointer to avoid redundant atomic loads when called under `m_mcips_mutex`. +- **Active vCPU Management**: Maintains an atomic pointer (`m_active_vcpu`) to the currently active (executing) vCPU + +#### **Translation Block Instrumentation** +```cpp +// For each TB, the plugin: +// 1. Counts instructions inline (atomic increment) +m_inst.plugin_api().qemu_plugin_register_vcpu_tb_exec_inline_per_vcpu( + tb, QEMU_PLUGIN_INLINE_ADD_U64, delta_insn, n_insns); + +// 2. Registers conditional callback when quota reached +m_inst.plugin_api().qemu_plugin_register_vcpu_tb_exec_cond_cb( + tb, &bridge_vcpu_tb_exec_cond, QEMU_PLUGIN_CB_NO_REGS, + QEMU_PLUGIN_COND_GE, delta_insn, m_global_quantum, handle_as_userdata()); +``` + +#### **Multi-vCPU State Management** +- **vCPU States (`vCPUTimeStatus`)**: Each vCPU can be in `IDLE`, `RUNNING`, or `PAUSED` state +- **Per-vCPU data (`vCPUTime` struct)**: Tracks `index`, `insn_per_second`, `delta_insn`, `cpu_time`, `cpu_execution_status` +- **Active vCPU Selection**: When the active CPU goes idle, `select_active_vcpu()` picks the next available non-idle, non-halted CPU +- **Slowest CPU Tracking**: `slowest_active_cpu()` finds the CPU that is furthest behind in simulation time +- **Pause/Resume Logic**: `pause_if_ahead()` pauses vCPUs that advance too far ahead; `resume_if_behind()` resumes those that fall behind. The active CPU pointer is loaded once and threaded through both functions to minimize atomic contention. + +#### **Idle Time Pump** +When all CPUs are idle (e.g., all in WFI/WFE state), the plugin starts an **idle time pump** (`m_idle_tick` / `idle_tick_method()`) so that QEMU timers can still fire and move simulation time forward. The pump is triggered by `get_qemu_clock()` being called from QEMU's timer thread and stops on its own when QEMU stops calling. `vcpu_resume()` stops the pump by clearing `m_all_cpus_idle`. The `idle_tick_method` SC_METHOD is intentionally minimal, it is a pure "sink" for the async_event so that `sc_time_stamp()` advances. + +#### **Atomic Time Copy** +`m_qemu_time_ns` is an atomic copy of `m_qemu_time` in nanoseconds. It lets `get_qemu_clock()` (which runs on QEMU's timer thread without holding `m_mcips_mutex`) read the time safely. It is updated by `sync_qemu_time_ns()` every time `m_qemu_time` changes while holding `m_mcips_mutex`. + +#### **Shutdown and Cleanup** + +`McipsPlugin` implements deterministic shutdown through multiple guards: + +- **`m_shutdown` (atomic bool)**: Set during `end_of_simulation()` and checked at the start of every callback so late calls exit early. +- **`m_inflight_cb` (atomic int)**: Counts how many `receive_window_cb` calls are running right now; shutdown waits for this to reach zero before continuing. +- **`shutdown_bridge()`**: Called from both `end_of_simulation()` and the destructor. Safe to call more than once (the second call does nothing). +- **Detach on shutdown**: If the sync window is still attached, it is detached with the current `m_qemu_time` so SystemC can keep running on its own. + +##### Shutdown Double-Check Pattern + +`receive_window_cb` uses a double-check pattern to safely interact with +the `m_inflight_cb` counter during shutdown: + +``` +1. Check m_shutdown → if true, return early (no increment) +2. Increment m_inflight_cb +3. Re-check m_shutdown → if true, decrement and return +4. ... do work ... +5. Decrement m_inflight_cb +``` + +This prevents a race where `shutdown_cleanup()` finishes draining +`m_inflight_cb` before a concurrent `receive_window_cb` has incremented +it, which would cause a use-after-free. The second check in step 3 +catches the case where shutdown started between steps 1 and 2. + +*** + +### 3. Timing Parameters and Synchronization Accuracy + +The MCIPS plugin's synchronization behavior is controlled by two critical timing parameters: + +#### **quantum_ns Parameter** + +The `quantum_ns` parameter defines the global quantum (in nanoseconds) used for time synchronization between SystemC and QEMU. This parameter is set at the platform level: + +```lua +platform = { + quantum_ns = 100000; -- Recommended: 100 microseconds + -- ... other platform configuration +} +``` + +**Impact on Synchronization:** + +The `quantum_ns` value controls the trade-off between synchronization accuracy and performance. Smaller values increase synchronization frequency and timing precision at the cost of overhead. Larger values reduce overhead but decrease timing accuracy. + +**Recommendation**: Use `quantum_ns = 100000` (100 microseconds) as a starting point. + +#### **insn_per_second Parameter** + +The `insn_per_second` parameter specifies the instruction execution rate for each vCPU, controlling the simulated CPU performance. This parameter is configured per CPU: + +```lua +cpu = { + moduletype = "cpu_arm_cortexA76"; + insn_per_second = 100000000; -- 100 MIPS + -- ... other CPU configuration +} +``` + +**Impact on CPU Timing:** + +This parameter directly determines how CPU time is calculated from instruction counts. The MCIPS plugin converts executed instructions into simulation time using: `cpu_time = (instructions_executed / insn_per_second) * 1e9` nanoseconds. + +*** + +### 4. Why Disable Quantum Keeper + +When MCIPS plugin is enabled, the traditional **quantum keeper mechanism is automatically disabled** in the CPU implementation (`cpu.h`). This is essential because: + +- **Quantum Keeper**: Uses wall-clock time and SystemC's quantum-based synchronization +- **MCIPS Plugin**: Uses instruction-count-based time calculation with custom synchronization windows + +#### **Automatic Disabling Logic** +```cpp +// In cpu.h - quantum keeper creation is conditional +inline bool mcips_enabled() const { return m_inst.is_mcips_enabled(); } + +void create_quantum_keeper() /* if mcips enabled, this function won't be called */ +{ + m_qk = m_inst.create_quantum_keeper(); + // ... quantum keeper setup +} + +// All quantum keeper related functions are bypassed when mcips_enabled() returns true +if (!mcips_enabled()) { + create_quantum_keeper(); + set_coroutine_mode(); + // ... other quantum keeper setup +} +``` + +When MCIPS is enabled, `cpu.h` also skips: coroutine spawning, `end_of_loop_cb`/`kick_cb`/`deadline_timer_cb` registration, the external-event watch thread, and all quantum-keeper start/stop/sync calls throughout the CPU lifecycle (constructor, `before_end_of_elaboration`, `start_of_simulation`, `halt_cb`, `reset_cb`, destructor, etc.). The `initiator_set_local_time()` method returns early, and `initiator_get_local_time()` skips the quantum-keeper path. + +#### **Halt and Reset with MCIPS** + +When MCIPS is active, `halt_cb` in `cpu.h` skips all quantum-keeper operations (timer, QK stop/start, kick event) and simply calls `lock_iothread` / `m_cpu.halt(val)` / `unlock_iothread` directly. `reset_cb` similarly skips QK start/reset and the kick event after reset. + +#### **Tracked Async Work** + +The CPU uses `make_tracked_async_job()` to wrap async jobs with `m_async_work_outstanding` tracking. The destructor waits (with a 500 ms timeout) for all in-flight jobs to complete before destroying the object, preventing use-after-free when async jobs hold captured references. + +### 5. Monitor Support + +The MCIPS plugin provides comprehensive **monitoring and debugging support** through the SystemC monitor interface: + +#### **Monitor Integration** +```cpp +// In monitor.cc - automatic discovery and registration +void monitor::end_of_elaboration() +{ + m_qks = find_sc_objects(); + m_mcips_plugins = find_sc_objects(); // Auto-discover MCIPS plugins +} +``` + +#### **Diagnostic JSON** + +`get_mcips_status_json()` returns a JSON snapshot of the current state (reads without `m_mcips_mutex` so values may be slightly out of date) including: `qemu_time`, `n_cpus`, `active_vcpu_index`, and per-CPU details (`index`, `insn_per_second`, `delta_insn`, `cpu_time_ns`, `cpu_execution_status`). + +*** + +### 6. SystemC Synchronization with sc_sync_window + +The MCIPS plugin uses **`sc_core::sc_sync_window`** (defined in `sync_window.h`) for coordinated time advancement between QEMU and SystemC. This is a template class parameterised by a sync policy. + +#### **sc_sync_window Architecture** +- **Template Policy**: `sc_sync_window` takes a sync policy as a template (e.g., `sc_sync_policy_tlm_quantum` uses the TLM global quantum, `sc_sync_policy_in_sync` tracks pending work). The policy tells the window how big each step is and whether to stay attached when idle. +- **Single Window**: The MCIPS plugin has one `sc_sync_window` (`m_sync_sc`) that talks to SystemC. +- **Time Windows**: Each window has a `{from, to}` range stored in `sc_current_window`. +- **Sweep/Step Model**: Uses `SC_METHOD` helpers inside -- `sweep_helper()` moves time to the start of the next window, `step_helper()` pauses SystemC at the end until a new window comes in. +- **Observer Event**: Uses `sc_ob_event` (or `gs::observer_event` as fallback) to notify at window edges. +- **Callback Registration**: Uses `register_sync_cb()` to get window updates through `receive_window_cb()`. + +#### **Attach and Detach** + +**Attach** (`m_sync_sc.attach()`): +- Turns on time synchronization between QEMU and SystemC. +- Used when at least one CPU is active and running. +- After attaching, you must call `async_set_window()` to send the first window. + +**Detach** (`m_sync_sc.detach(current_time)`): +- Opens the window to `[current_time, max_time]` and stops synchronization. +- Used when all CPUs are idle (e.g., all in WFI/WFE state). +- SystemC can keep running without waiting for QEMU. + +> **Important**: `async_set_window()` will crash if the window is not attached. Always attach first. + +#### **Synchronization Flow** +```cpp +// When vCPU goes idle and no other vCPUs are active +if (!new_active) { + if (m_sync_sc.is_attached()) { + m_sync_sc.detach(m_qemu_time); + } + m_all_cpus_idle.store(true, std::memory_order_release); + m_idle_tick.notify(m_quantum); // start idle pump +} + +// When vCPU resumes from idle (first to wake) +if (m_active_vcpu.load() == nullptr) { + m_active_vcpu.store(vcpu); + if (!m_sync_sc.is_attached()) { + m_sync_sc.attach(); + } + set_systemc_window(); // push [qemu_time, qemu_time + quantum] +} +``` + +*** + +### 7. QEMU TCG Plugin API Integration and Event Registration + +The MCIPS plugin leverages QEMU's **TCG Plugin API** through a sophisticated C++/C bridge system: + +#### **libidlinker Plugin and ID Communication** +QBox runs QEMU's **libidlinker** plugin to obtain a unique plugin ID: +- **Plugin Loading**: QBox loads `libidlinker.so` (Linux) or `libidlinker.dylib` (macOS) into QEMU +- **ID Generation**: libidlinker generates a unique `qemu_plugin_id_t` for the plugin instance +- **ID Communication**: libidlinker calls the C function `global_set_cci_param(key, plugin_id)` to communicate the ID back to QBox +- **CCI Integration**: The ID is stored as a `cci::cci_param` and used by the McipsPlugin instance + +#### **TCG Plugin API Access** +QBox calls QEMU's plugin API through the auto-generated `LibQemuExports` table, reached via the `LibQemu::plugin_api()` accessor: +- **Auto-generated dispatch table**: Plugin function pointers are populated by libqemu's `exports.py` machinery alongside the rest of the libqemu API. +- **Type Safety**: Plugin types (`qemu_plugin_id_t`, `qemu_plugin_tb`, callback typedefs) come directly from `qemu-plugin.h` via a glib-safe wrapper, so consumers don't need to redeclare anything. + +#### **LibQemuPlugin Base Class** + +The **LibQemuPlugin** base class (`libqemu-plugin.h`) inherits from `sc_core::sc_module` and holds a reference to `qemu::LibQemu`. It provides: + +- **Instance map**: `id_map()` and `map_mutex()` are heap-allocated and never deleted, so they stay alive even after `main()` returns. This avoids crashes when QEMU threads call back into us during process exit. Used only by the ID-keyed dispatch path. +- **PluginHandle**: Heap-allocated, intentionally leaked at plugin destruction. USERDATA-keyed callbacks pass `handle_as_userdata()` so static bridges can resolve to the C++ instance with no map lookup. Outliving the plugin lets stale callbacks observe `alive == false` and exit early instead of dereferencing freed memory. +- **REGISTER_CALLBACK_ID macro**: Generates a virtual no-op, a static bridge that resolves the plugin via `id_map`, and an `enable_*` registration helper. +- **USERDATA-keyed callbacks**: No macro. Subclasses write a static bridge that calls `LibQemuPlugin::dispatch_userdata(userdata, lambda)` and register it directly via `m_inst.plugin_api().qemu_plugin_register_*_cb(...)`. +- **`shutdown_bridge()`**: Stops both bridge dispatch paths and waits for in-flight callbacks to drain. Idempotent — `m_handle->alive`'s exchange is the once-guard. Nulls the `id_map` entry, then spins until `m_handle->refcount` reaches zero. +- **ID-keyed callback surface**: `vcpu_init`, `vcpu_exit`, `vcpu_idle`, `vcpu_resume`, `vcpu_tb_trans`, `vcpu_syscall`, `vcpu_syscall_ret`. USERDATA-keyed callbacks (`vcpu_tb_exec_cond`, `register_time_cb`, ...) are registered directly at the call site. + +#### **Event Registration Process** +The MCIPS plugin registers multiple event types during `end_of_elaboration()`: + +```cpp +// ID-keyed callbacks registered through the macro-generated enable_* helpers +enable_vcpu_tb_trans(m_id, &bridge_vcpu_tb_trans); +enable_vcpu_init(m_id, &bridge_vcpu_init); +enable_vcpu_resume(m_id, &bridge_vcpu_resume); +enable_vcpu_idle(m_id, &bridge_vcpu_idle); + +// Time callback registered directly with handle_as_userdata() +m_inst.plugin_api().qemu_plugin_register_time_cb( + m_time_handle, &bridge_get_qemu_clock, handle_as_userdata()); +``` + +### How Everything Works Together + +The MCIPS plugin creates a sophisticated multi-layered synchronization system: + +1. **QEMU Layer**: libidlinker plugin provides unique ID; TCG instrumentation counts instructions inline +2. **Bridge Layer**: C++/C macros and bridge functions pass QEMU callbacks to SystemC components; refcounting keeps objects alive during callbacks +3. **Plugin Layer**: McipsPlugin turns instruction counts into simulation time, manages CPU states (IDLE/RUNNING/PAUSED), and runs the idle time pump +4. **SystemC Layer**: `sc_sync_window` talks to the SystemC kernel through sweep/step methods to move time forward +5. **Monitor Layer**: Provides visibility into the entire system state via JSON diagnostics + +*** + ## Quick Start: Ubuntu Platform (AArch64) ```bash @@ -225,3 +560,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md). ## License See [LICENSE](LICENSE). + diff --git a/package-lock.cmake b/package-lock.cmake index 205c1955..4cc17888 100644 --- a/package-lock.cmake +++ b/package-lock.cmake @@ -27,7 +27,7 @@ CPMDeclarePackage(SCP CPMDeclarePackage(qemu NAME libqemu GIT_REPOSITORY ${LIBQEMU_GIT} - GIT_TAG libqemu-v11.0-v0.6 + GIT_TAG libqemu-v11.0-v0.7 GIT_SUBMODULES CMakeLists.txt GIT_SHALLOW ON ) diff --git a/qemu-components/common/include/cpu.h b/qemu-components/common/include/cpu.h index 0f87b984..bbb24ba7 100644 --- a/qemu-components/common/include/cpu.h +++ b/qemu-components/common/include/cpu.h @@ -29,6 +29,9 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface { +private: + inline bool mcips_enabled() const { return m_inst.is_mcips_enabled(); } // mcips: multi core instructions per second + protected: /* * We have a unique copy per CPU of this extension, which is not dynamically allocated. @@ -62,6 +65,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface std::mutex m_can_delete; QemuCpuHintTlmExtension m_cpu_hint_ext; + cci::cci_param m_insn_per_second; uint64_t m_quantum_ns; // For convenience @@ -118,7 +122,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface /* * Request quantum keeper from instance */ - void create_quantum_keeper() + void create_quantum_keeper() /* if mcips enabeld, this function won't be called */ { m_qk = m_inst.create_quantum_keeper(); @@ -133,7 +137,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface * Given the quantum keeper nature (synchronous or asynchronous) and the * p_icount parameter, we can configure the QEMU instance accordingly. */ - void set_coroutine_mode() + void set_coroutine_mode() /* if mcips enabeld, this function won't be called */ { switch (m_qk->get_thread_type()) { case gs::SyncPolicy::SYSTEMC_THREAD: @@ -161,7 +165,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface * while the CPU thread go to sleep, the fact that the CPU thread is also * the SystemC thread will ensure correct ordering of the events. */ - void set_signaled() + void set_signaled() /* if mcips enabeld, this function won't be called */ { assert(!m_coroutines); if (m_inst.get_tcg_mode() != QemuInstance::TCG_SINGLE) { @@ -179,7 +183,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface * SystemC thread watching the m_external_ev event list. Only used in MTTCG * mode. */ - void watch_external_ev() + void watch_external_ev() /* if mcips enabeld, this function won't be called */ { for (;;) { wait(m_external_ev); @@ -191,7 +195,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface * Called when the CPU is kicked. We notify the corresponding async event * to wake the CPU up if it was sleeping waiting for work. */ - void kick_cb() + void kick_cb() /* if mcips enabeld, this function won't be called */ { SCP_TRACE(())("QEMU deadline KICK callback"); if (m_coroutines) { @@ -207,7 +211,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface * However, we should also handle the case that qemu is currently in 'sync' * - by setting the time here, we will nudge the sync thread. */ - void deadline_timer_cb() + void deadline_timer_cb() /* if mcips enabeld, this function won't be called */ { SCP_TRACE(())("QEMU deadline timer callback"); // All syncing will be done in end_of_loop_cb @@ -234,7 +238,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface * - In MTTCG mode, we wait on the m_signaled_cond condition, signaled when * set_signaled is called. */ - void wait_for_work() + void wait_for_work() /* if mcips enabeld, this function won't be called */ { SCP_TRACE(())("Wait for work"); m_qk->stop(); @@ -261,7 +265,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface /* * Set the deadline timer to trigger at the end of the time budget */ - void rearm_deadline_timer() + void rearm_deadline_timer() /* if mcips enabeld, this function won't be called */ { // This is a simple "every quantum" tick. Whether the QK makes use of it or not // is down to the sync policy @@ -272,7 +276,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface * Called before running the CPU. Lock the BQL and set the deadline timer * to not run beyond the time budget. */ - void prepare_run_cpu() + void prepare_run_cpu() /* if mcips enabeld, this function won't be called */ { /* * The QEMU CPU loop expect us to enter it with the iothread mutex locked. @@ -313,7 +317,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface /* * Run the CPU loop. Only used in coroutine mode. */ - void run_cpu_loop() + void run_cpu_loop() /* if mcips enabeld, this function won't be called */ { auto last_vclock = m_inst.get().get_virtual_clock(); m_cpu.loop(); @@ -332,7 +336,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface /* * Called after a CPU loop run. It synchronizes with the kernel. */ - void sync_with_kernel() + void sync_with_kernel() /* if mcips enabeld, this function won't be called */ { int64_t now = m_inst.get().get_virtual_clock(); @@ -356,7 +360,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface * mode, we yield here to come back to run_cpu_loop(). In TCG thread mode, * we use this hook to synchronize with the kernel. */ - void end_of_loop_cb() + void end_of_loop_cb() /* if mcips enabeld, this function won't be called */ { SCP_TRACE(())("End of loop"); if (m_finished) return; @@ -379,7 +383,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface /* * SystemC thread entry when running in coroutine mode. */ - void mainloop_thread_coroutine() + void mainloop_thread_coroutine() /* if mcips enabeld, this function won't be called */ { m_cpu.register_thread(); @@ -406,23 +410,25 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface , m_signaled(false) , p_gdb_port("gdb_port", 0, "Wait for gdb connection on TCP port ") , socket("mem", *this, inst) + , m_insn_per_second("insn_per_second", 1'000'000'000, "number of instructions per second in mcips mode") + , m_coroutines(false) { using namespace std::placeholders; - - m_external_ev |= m_qemu_kick_ev; - + if (!mcips_enabled()) { + m_external_ev |= m_qemu_kick_ev; + } auto haltcb = std::bind(&QemuCpu::halt_cb, this, _1); halt.register_value_changed_cb(haltcb); auto resetcb = std::bind(&QemuCpu::reset_cb, this, _1); reset.register_value_changed_cb(resetcb); - create_quantum_keeper(); - set_coroutine_mode(); - - if (!m_coroutines) { - SC_THREAD(watch_external_ev); + if (!mcips_enabled()) { + create_quantum_keeper(); + set_coroutine_mode(); + if (!m_coroutines) { + SC_THREAD(watch_external_ev); + } } - m_inst.add_dev(this); m_start_reset_done_ev.async_detach_suspending(); @@ -448,8 +454,10 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface } } - while (!m_can_delete.try_lock()) { - m_qk->stop(); + if (!mcips_enabled()) { + while (!m_can_delete.try_lock()) { + m_qk->stop(); + } } m_inst.del_dev(this); } @@ -493,10 +501,11 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface } /* Unblock it if it's waiting for run budget */ - m_qk->stop(); - - /* Unblock the CPU thread if it's sleeping */ - set_signaled(); + if (!mcips_enabled()) { + m_qk->stop(); + /* Unblock the CPU thread if it's sleeping */ + set_signaled(); + } /* Wait for QEMU to terminate the CPU thread */ /* @@ -521,20 +530,21 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface QemuDevice::before_end_of_elaboration(); m_cpu = qemu::Cpu(m_dev); - - if (m_coroutines) { - m_sc_thread = sc_core::sc_spawn(std::bind(&QemuCpu::mainloop_thread_coroutine, this)); + if (!mcips_enabled()) { + if (m_coroutines) { + m_sc_thread = sc_core::sc_spawn(std::bind(&QemuCpu::mainloop_thread_coroutine, this)); + } } - socket.init(m_dev, "memory"); m_cpu.set_soft_stopped(true); - m_cpu.set_end_of_loop_callback(std::bind(&QemuCpu::end_of_loop_cb, this)); - m_cpu.set_kick_callback(std::bind(&QemuCpu::kick_cb, this)); - - m_deadline_timer = m_inst.get().timer_new(); - m_deadline_timer->set_callback(std::bind(&QemuCpu::deadline_timer_cb, this)); + if (!mcips_enabled()) { + m_cpu.set_end_of_loop_callback(std::bind(&QemuCpu::end_of_loop_cb, this)); + m_cpu.set_kick_callback(std::bind(&QemuCpu::kick_cb, this)); + m_deadline_timer = m_inst.get().timer_new(); + m_deadline_timer->set_callback(std::bind(&QemuCpu::deadline_timer_cb, this)); + } m_cpu_hint_ext.set_cpu(m_cpu); } @@ -543,17 +553,21 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface { SCP_TRACE(())("Halt : {}", val); if (!m_finished) { - if (val) { - m_deadline_timer->del(); - m_qk->stop(); - } else { - m_qk->start(); - rearm_deadline_timer(); + if (!mcips_enabled()) { + if (val) { + m_deadline_timer->del(); + m_qk->stop(); + } else { + m_qk->start(); + rearm_deadline_timer(); + } } m_inst.get().lock_iothread(); m_cpu.halt(val); m_inst.get().unlock_iothread(); - m_qemu_kick_ev.async_notify(); // notify the other thread so that the CPU is allowed to continue + if (!mcips_enabled()) { + m_qemu_kick_ev.async_notify(); // notify the other thread so that the CPU is allowed to continue + } } } @@ -576,24 +590,33 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface if (m_resetting == none) return; // dont finish a finished reset! while (m_resetting == start_reset) { SCP_WARN(())("Hold reset"); + m_cpu.kick(); // without this kick, async_safe_run may not be called (QEMU race) sc_core::wait(m_start_reset_done_ev); } m_inst.get().lock_iothread(); socket.reset(); // remove DMI's (needs BQL for memory region updates) m_inst.get().unlock_iothread(); m_cpu.reset(false); // call the end-of-reset (which will unpause the CPU) - m_qk->start(); // restart the QK if it's stopped - m_qk->reset(); - m_qemu_kick_ev.async_notify(); // notify the other thread so that the CPU is allowed to continue + if (!mcips_enabled()) { + m_qk->start(); // restart the QK if it's stopped + m_qk->reset(); + m_qemu_kick_ev.async_notify(); // notify the other thread so that the CPU is allowed to continue + } SCP_WARN(())("Finished reset"); m_resetting = none; } - m_qemu_kick_ev.async_notify(); // notify the other thread so that the CPU is allowed to process if required + if (!mcips_enabled()) { + m_qemu_kick_ev.async_notify(); // notify the other thread so that the CPU is allowed to process if required + } } virtual void end_of_elaboration() override { QemuDevice::end_of_elaboration(); - + if (mcips_enabled()) { + if (!m_inst.get_mcips_plugin().set_vcpu_insn_per_second(m_cpu.get_index(), m_insn_per_second)) { + SCP_FATAL(()) << "Failed to set insn_per_second for cpu_" << m_cpu.get_index(); + } + } if (!p_gdb_port.is_default_value()) { std::stringstream ss; SCP_INFO(()) << "Starting gdb server on TCP port " << p_gdb_port; @@ -608,7 +631,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface QemuDevice::start_of_simulation(); if (m_inst.get_tcg_mode() == QemuInstance::TCG_SINGLE) { - if (m_inst.can_run()) { + if (m_inst.can_run() && !mcips_enabled()) { m_qk->start(); } } else if (!m_coroutines) { @@ -620,7 +643,9 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface * run_on_sysc(). The QK will be stopped later in wait_for_work() * when the CPU halts (e.g. WFI), allowing normal starvation exit. */ - m_qk->start(); + if (!mcips_enabled()) { + m_qk->start(); + } } m_started = true; @@ -633,8 +658,9 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface * first called), leaving no suspending events and causing * premature simulation exit due to starvation. */ - m_qk->start(); - + if (!mcips_enabled()) { + m_qk->start(); + } /* Prepare the CPU for its first run and release it * Hold BQL to synchronize with the vCPU thread's idle-wait loop * in qemu_process_cpu_events(). That loop checks cpu_thread_is_idle() @@ -645,7 +671,9 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface */ m_inst.get().lock_iothread(); m_cpu.set_soft_stopped(false); - rearm_deadline_timer(); + if (!mcips_enabled()) { + rearm_deadline_timer(); + } m_cpu.kick(); m_inst.get().unlock_iothread(); } @@ -681,7 +709,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface vclock_now = m_inst.get().get_virtual_clock(); sc_core::sc_time sc_t = sc_core::sc_time_stamp(); - if (sc_time(vclock_now, SC_NS) > sc_t) { + if (sc_time(vclock_now, SC_NS) > sc_t && !mcips_enabled()) { m_qk->set(sc_time(vclock_now, SC_NS) - sc_t); return m_qk->get_local_time(); } else { @@ -695,6 +723,7 @@ class QemuCpu : public QemuDevice, public QemuInitiatorIface */ virtual void initiator_set_local_time(const sc_core::sc_time& t) override { + if (mcips_enabled()) return; if (m_finished) return; m_qk->set(t); diff --git a/qemu-components/common/include/glib_shim/glib.h b/qemu-components/common/include/glib_shim/glib.h new file mode 100644 index 00000000..b58d8005 --- /dev/null +++ b/qemu-components/common/include/glib_shim/glib.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. All Rights Reserved. + * + * SPDX-License-Identifier: BSD-3-Clause-Clear + * + * Forward-decl-only glib.h shim, on libqemu-cxx's PRIVATE include path + * BEFORE the system glib path. + * + * Why: libqemu's generated typedefs.h is wrapped in extern "C" by + * libqemu/libqemu.h, and qemu-plugin.h does `#include `. Modern + * glib.h has C++ template inline functions (g_steal_pointer, ...) that + * fail with "template with C linkage" inside extern "C". qbox's plugin + * API surface only references @c GArray and @c GByteArray as opaque + * pointers, so opaque forward-decls are sufficient — and shadowing real + * glib for the libqemu-cxx target only is safe because nothing in qbox + * C++ uses real glib internals. + */ +#ifndef GLIB_QBOX_SHIM_H +#define GLIB_QBOX_SHIM_H + +typedef struct _GArray GArray; +typedef struct _GByteArray GByteArray; + +#endif diff --git a/qemu-components/common/include/libqemu-cxx/libqemu-cxx.h b/qemu-components/common/include/libqemu-cxx/libqemu-cxx.h index a97d19e6..00a6b249 100644 --- a/qemu-components/common/include/libqemu-cxx/libqemu-cxx.h +++ b/qemu-components/common/include/libqemu-cxx/libqemu-cxx.h @@ -21,6 +21,11 @@ #include #include +/* Brings in the generated LibQemuExports struct and all plugin types + * (qemu_plugin_id_t, qemu_plugin_tb, qemu_plugin_cb_flags, callbacks, ...) + */ +#include + #include /* libqemu types forward declaration */ @@ -112,6 +117,9 @@ class LibQemu LibQemu(LibraryLoaderIface& library_loader, Target t); ~LibQemu(); + /* TCG plugin API: returns the libqemu-exports table. */ + const LibQemuExports& plugin_api() const; + void push_qemu_arg(const char* arg); void push_qemu_arg(std::initializer_list args); const std::vector& get_qemu_args() const { return m_qemu_argv; } diff --git a/qemu-components/common/include/libqemu-plugin.h b/qemu-components/common/include/libqemu-plugin.h new file mode 100644 index 00000000..d36076ef --- /dev/null +++ b/qemu-components/common/include/libqemu-plugin.h @@ -0,0 +1,252 @@ +/* + * This file is part of libqbox + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. All Rights Reserved. + * + * SPDX-License-Identifier: BSD-3-Clause-Clear + * + * Bridges QEMU's C plugin callbacks into the owning C++ plugin instance. + * + * Two dispatch paths, one shared safety net: + * + * - ID-keyed: callbacks where QEMU only carries `qemu_plugin_id_t` + * (vcpu_init, vcpu_idle, vcpu_resume, vcpu_tb_trans, ...). + * Resolved by an `id_map` lookup. Use the + * REGISTER_CALLBACK_ID macro to declare them. + * + * - USERDATA-keyed: callbacks that take a `void* userdata` + * (vcpu_tb_exec_cond, register_time_cb, vcpu_mem, ...). + * Resolved with zero lookup: the userdata IS a + * `PluginHandle*`. Subclasses register the bridge + * directly and call `dispatch_userdata` inside it. + * + * The PluginHandle is heap-allocated and intentionally leaked at plugin + * destruction so that any callback QEMU still has registered finds + * `alive == false` and returns early instead of dereferencing a freed + * plugin object. Both paths bump the same `m_handle->refcount`, which + * `shutdown_bridge` drains to serialize destruction with in-flight + * callbacks. + */ + +#ifndef _LIBQBOX_COMPONENTS_LIBQEMU_PLUGIN_H +#define _LIBQBOX_COMPONENTS_LIBQEMU_PLUGIN_H + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +constexpr double NSEC_IN_ONE_SEC = 1e9; + +#define REMOVE_PARENS(...) REMOVE_PARENS_IMPL __VA_ARGS__ +#define REMOVE_PARENS_IMPL(...) __VA_ARGS__ + +class LibQemuPlugin; + +/** + * @brief Stable userdata for USERDATA-keyed plugin callbacks. + * + * Heap-allocated when the plugin is constructed and intentionally leaked + * when it is destroyed. Outliving the plugin lets stale callbacks observe + * @c alive == false and exit early without dereferencing freed memory. + */ +struct PluginHandle { + LibQemuPlugin* self{ nullptr }; ///< valid while @c alive == true + std::atomic alive{ true }; ///< false once shutdown_bridge() returns + std::atomic refcount{ 0 }; ///< in-flight bridge callbacks (both paths) +}; + +/** + * @brief Declare an ID-keyed bridge for one QEMU plugin callback. + * + * Generates: + * - virtual no-op @c cb_name() that subclasses override, + * - static @c bridge_##cb_name() that resolves the plugin via @c id_map(), + * - @c enable_##cb_name() that registers the bridge with libqemu. + */ +#define REGISTER_CALLBACK_ID(cb_name, bridge_cb_param_types, bridge_cb_params, reg_params_types, reg_params) \ + virtual void cb_name(qemu_plugin_id_t id, REMOVE_PARENS(bridge_cb_param_types)) {} \ + static void bridge_##cb_name(qemu_plugin_id_t id, REMOVE_PARENS(bridge_cb_param_types)) \ + { \ + LibQemuPlugin* self = LibQemuPlugin::acquire_instance(id); \ + if (!self) return; \ + self->cb_name(id, REMOVE_PARENS(bridge_cb_params)); \ + LibQemuPlugin::release_instance(self); \ + } \ + void enable_##cb_name reg_params_types { m_inst.plugin_api().qemu_plugin_register_##cb_name##_cb reg_params; } + +/** + * @brief Base class for QBox C++ plugins that bridge QEMU plugin callbacks. + */ +class LibQemuPlugin : public sc_core::sc_module +{ + SCP_LOGGER(); + +protected: + cci::cci_param m_id; ///< unique id supplied by configuration + qemu::LibQemu& m_inst; + + /// Leaked at destruction so stale callbacks find @c alive==false. + PluginHandle* m_handle; + + /// id -> plugin*. Heap-allocated to outlive main(); QEMU threads may + /// call back during process exit. Entry is set to nullptr at shutdown. + static std::unordered_map& id_map() + { + static auto* m = new std::unordered_map(); + return *m; + } + static std::shared_mutex& map_mutex() + { + static auto* m = new std::shared_mutex(); + return *m; + } + +public: + LibQemuPlugin(const sc_core::sc_module_name& nm, qemu::LibQemu& inst) + : sc_module(nm), m_inst(inst), m_id("id", 0, "qemu plugin id"), m_handle(new PluginHandle()) + { + m_handle->self = this; + } + + virtual ~LibQemuPlugin() { shutdown_bridge(); } + + /// Push the `-plugin ,key=` argument to libqemu's command line. + /// Called from the subclass after QemuInstance has finished pushing its + /// own libqbox arguments. + void push_plugin_args(const std::string& plugin_path) + { + SCP_DEBUG(())("push_plugin_args, key: {} ", m_id.name()); + SCP_DEBUG(())("push_plugin_args, plugin_path: {} ", plugin_path); + std::stringstream opts; + opts << plugin_path; + opts << ",key=" << m_id.name(); + m_inst.push_qemu_arg("-plugin"); + m_inst.push_qemu_arg(opts.str().c_str()); + } + + void end_of_elaboration() override + { + sc_assert(m_id); + SCP_DEBUG(())("LibQemuPlugin: key {}, id: {}", m_id.name(), m_id); + std::lock_guard lock(map_mutex()); + id_map()[m_id] = this; + } + + /** + * @brief Stop both bridge dispatch paths and drain in-flight callbacks. + * + * Idempotent — `m_handle->alive`'s exchange is the once-guard. Nulls the + * id_map entry so new ID lookups fail, then waits for the shared + * refcount to drain. The handle itself is intentionally leaked. + */ + void shutdown_bridge() + { + if (!m_handle->alive.exchange(false, std::memory_order_seq_cst)) { + return; + } + { + std::lock_guard lock(map_mutex()); + auto& m = id_map(); + auto it = m.find(m_id); + if (it != m.end()) { + it->second = nullptr; + } + } + while (m_handle->refcount.load(std::memory_order_acquire) > 0) { + std::this_thread::yield(); + } + } + + // + // USERDATA-keyed dispatch + // + + /// Pass this as @c userdata when registering a USERDATA-keyed callback. + void* handle_as_userdata() const { return m_handle; } + + /** + * @brief Run @p body on the plugin instance iff the handle is still alive. + * + * Safety contract for direct-registered USERDATA bridges. A subclass + * writes a static bridge that calls this: + * + * static void bridge_my_cb(int idx, void* userdata) { + * LibQemuPlugin::dispatch_userdata(userdata, [&](LibQemuPlugin* p) { + * static_cast(p)->my_cb(idx); + * }); + * } + * + * No-op when the handle is null or @c alive==false. + */ + template + static void dispatch_userdata(void* userdata, Body&& body) + { + auto* h = static_cast(userdata); + if (!h || !h->alive.load(std::memory_order_acquire)) return; + h->refcount.fetch_add(1, std::memory_order_seq_cst); + if (h->alive.load(std::memory_order_seq_cst)) { + std::forward(body)(h->self); + } + h->refcount.fetch_sub(1, std::memory_order_seq_cst); + } + + // + // ID-keyed dispatch + // + + /** + * @brief Look up a plugin by id and bump the shared refcount. + * @return Plugin pointer, or nullptr if not found / shut down. + * Caller must call release_instance() when done. + */ + static LibQemuPlugin* acquire_instance(qemu_plugin_id_t plugin_id) + { + std::shared_lock lock(map_mutex()); + auto& m = id_map(); + auto it = m.find(plugin_id); + if (it == m.end() || !it->second) return nullptr; + auto* self = it->second; + // Relaxed: shared_lock provides the acquire-side visibility. + self->m_handle->refcount.fetch_add(1, std::memory_order_relaxed); + return self; + } + + /// Drop the shared refcount after an ID-keyed bridge completes. + static void release_instance(LibQemuPlugin* self) noexcept + { + self->m_handle->refcount.fetch_sub(1, std::memory_order_release); + } + + REGISTER_CALLBACK_ID(vcpu_init, (unsigned int vcpu_index), (vcpu_index), + (qemu_plugin_id_t id, qemu_plugin_vcpu_simple_cb_t cb), (id, cb)) + REGISTER_CALLBACK_ID(vcpu_exit, (unsigned int vcpu_index), (vcpu_index), + (qemu_plugin_id_t id, qemu_plugin_vcpu_simple_cb_t cb), (id, cb)) + REGISTER_CALLBACK_ID(vcpu_idle, (unsigned int vcpu_index), (vcpu_index), + (qemu_plugin_id_t id, qemu_plugin_vcpu_simple_cb_t cb), (id, cb)) + REGISTER_CALLBACK_ID(vcpu_resume, (unsigned int vcpu_index), (vcpu_index), + (qemu_plugin_id_t id, qemu_plugin_vcpu_simple_cb_t cb), (id, cb)) + REGISTER_CALLBACK_ID(vcpu_tb_trans, (qemu_plugin_tb * tb), (tb), + (qemu_plugin_id_t id, qemu_plugin_vcpu_tb_trans_cb_t cb), (id, cb)) + REGISTER_CALLBACK_ID(vcpu_syscall, + (unsigned int vcpu_index, int64_t num, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, + uint64_t a5, uint64_t a6, uint64_t a7, uint64_t a8), + (vcpu_index, num, a1, a2, a3, a4, a5, a6, a7, a8), + (qemu_plugin_id_t id, qemu_plugin_vcpu_syscall_cb_t cb), (id, cb)) + REGISTER_CALLBACK_ID(vcpu_syscall_ret, (unsigned int vcpu_idx, int64_t num, int64_t ret), (vcpu_idx, num, ret), + (qemu_plugin_id_t id, qemu_plugin_vcpu_syscall_ret_cb_t cb), (id, cb)) + + // qemu_plugin_register_flush_cb — not used; doesn't fit the callback pattern. + // qemu_plugin_register_atexit_cb — not used; conflicts with QBox's exit handling. +}; + +#endif // _LIBQBOX_COMPONENTS_LIBQEMU_PLUGIN_H diff --git a/qemu-components/common/include/mcips-plugin.h b/qemu-components/common/include/mcips-plugin.h new file mode 100644 index 00000000..b77e3677 --- /dev/null +++ b/qemu-components/common/include/mcips-plugin.h @@ -0,0 +1,784 @@ +/* + * This file is part of libqbox + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. All Rights Reserved. + * + * SPDX-License-Identifier: BSD-3-Clause-Clear + */ + +#ifndef _LIBQBOX_COMPONENTS_MCIPS_PLUGIN_H +#define _LIBQBOX_COMPONENTS_MCIPS_PLUGIN_H + +#include "libqemu-plugin.h" +#include +#include +#include +#include +#include +#include +#include + +class McipsPlugin : public LibQemuPlugin +{ + SCP_LOGGER(); + SC_HAS_PROCESS(McipsPlugin); + +private: + enum vCPUTimeStatus { IDLE, PAUSED, RUNNING }; + + struct vCPUTime { + uint64_t index; + uint64_t insn_per_second; + uint64_t delta_insn; + sc_core::sc_time cpu_time; + vCPUTimeStatus cpu_execution_status; + }; + + uint64_t m_global_quantum; // quantum in nanoseconds; used as the instruction count limit + int m_num_vcpus{ 0 }; // cached CPU count (set in end_of_elaboration, may grow in vcpu_init) + void* m_time_handle; // handle returned by QEMU to let us control time + sc_core::sc_sync_window m_sync_sc; // SystemC sync window + sc_core::sc_sync_window::window + sc_current_window; // last window received from SystemC + sc_core::sc_time m_quantum; // copy of the TLM global quantum + sc_core::sc_time m_qemu_time; // main QEMU simulation time (only changed while holding m_mcips_mutex) + /* CPU currently driving m_qemu_time. Read lock-free by get_qemu_clock() (acquire/release). */ + std::atomic m_active_vcpu; + qemu_plugin_scoreboard* m_vcpus_scoreboard; // one vCPUTime entry per CPU index + + /* Nanosecond shadow of m_qemu_time; sc_time is not thread-safe. */ + std::atomic m_qemu_time_ns{ 0 }; + + /* Shutdown flag; all entry points return immediately when set. */ + std::atomic m_shutdown{ false }; + + /* Counts in-flight receive_window_cb() calls; shutdown waits for zero. */ + std::atomic m_inflight_cb{ 0 }; + + /* Protects shared state across vCPU and SystemC threads. */ + std::mutex m_mcips_mutex; + + /* Fires a SystemC event to keep time moving when all CPUs are idle. + * get_qemu_clock() re-schedules it; vcpu_resume() stops it. */ + gs::async_event m_idle_tick; + + /* True while all CPUs are idle; drives the idle-tick pump. */ + std::atomic m_all_cpus_idle{ false }; + + /* False until the first vCPU is initialized; drives unconditional idle-tick pump. */ + std::atomic m_first_vcpu_initialized{ false }; + + /* Iothread-livelock watchdog state. Wall-clock gated (1s) so sub-second + * unit tests never trip it. */ + std::atomic m_wd_last_clock_ns{ 0 }; + std::atomic m_wd_freeze_start_ns{ 0 }; + sc_core::sc_time m_wd_last_tick_qemu_time{ sc_core::SC_ZERO_TIME }; // SC-side, under m_mcips_mutex + static constexpr int64_t kWdFreezeThresholdNs = 1'000'000'000LL; + +public: + McipsPlugin(const sc_core::sc_module_name& nm, qemu::LibQemu& inst) + : LibQemuPlugin(nm, inst) + , m_sync_sc("m_sync_sc") + , m_global_quantum(0) + , m_time_handle(nullptr) + , sc_current_window(sc_core::sc_sync_window::zero_window) + , m_quantum(sc_core::SC_ZERO_TIME) + , m_qemu_time(sc_core::SC_ZERO_TIME) + , m_active_vcpu(nullptr) + , m_vcpus_scoreboard(nullptr) + , m_idle_tick(false) + { + SC_METHOD(idle_tick_method); + dont_initialize(); + sensitive << m_idle_tick; + } + + /** + * @brief SC_METHOD triggered by m_idle_tick; lets SystemC advance time while all CPUs are idle, + * or (active-CPU branch) recovers from the iothread livelock kicked by get_qemu_clock. + */ + void idle_tick_method() + { + std::unique_lock lock(m_mcips_mutex); + + // Watchdog recovery: kicked by get_qemu_clock when m_qemu_time has been frozen >1s wall. + // If qemu_time hasn't advanced since the last tick, bump by one quantum so the iothread's + // next gt_recalc_timer reads a fresh deadline, ppoll sleeps, BQL is released. + auto* active = m_active_vcpu.load(std::memory_order_relaxed); + if (active) { + const sc_core::sc_time current_qemu_time = qemu_time_now(active); + if (current_qemu_time == m_wd_last_tick_qemu_time && m_quantum > sc_core::SC_ZERO_TIME) { + m_qemu_time += m_quantum; + sync_qemu_time_ns(); + active->cpu_time += m_quantum; + set_systemc_window(); + m_idle_tick.notify(m_quantum); + } + m_wd_last_tick_qemu_time = current_qemu_time; + } + } + + /** + * @brief Shared shutdown logic used by both the destructor and end_of_simulation(). + * + * Stops the idle pump, marks shutdown, clears the active CPU, waits for + * any in-flight receive_window_cb to finish, and detaches the sync window. + * Safe to call more than once (subsequent calls are no-ops). + */ + void shutdown_cleanup() + { + if (m_shutdown.load(std::memory_order_seq_cst)) { + return; // already shut down + } + + m_all_cpus_idle.store(false, std::memory_order_release); // stop idle pump + m_shutdown.store(true, std::memory_order_seq_cst); // must be seq_cst before inflight drain + m_active_vcpu.store(nullptr, std::memory_order_release); + + shutdown_bridge(); + + /* Wait for any in-flight receive_window_cb() to finish. */ + while (m_inflight_cb.load(std::memory_order_seq_cst) > 0) { + std::this_thread::yield(); + } + + detach_sync_window(); + } + + ~McipsPlugin() { shutdown_cleanup(); } + + /** @brief Called by SystemC at end of simulation; stops all QEMU callbacks. */ + void end_of_simulation() override { shutdown_cleanup(); } + + /** @brief Set up the plugin, register QEMU callbacks, and send the first sync window. */ + void end_of_elaboration() override + { + LibQemuPlugin::end_of_elaboration(); + m_vcpus_scoreboard = m_inst.plugin_api().qemu_plugin_scoreboard_new(sizeof(vCPUTime)); + sc_assert(m_vcpus_scoreboard); + + m_time_handle = const_cast(m_inst.plugin_api().qemu_plugin_request_time_control(m_id)); + sc_assert(m_time_handle); + + m_quantum = tlm_utils::tlm_quantumkeeper::get_global_quantum(); + m_global_quantum = static_cast(std::floor(m_quantum.to_seconds() * NSEC_IN_ONE_SEC)); + + sc_current_window = { sc_core::SC_ZERO_TIME, m_quantum }; + if (m_sync_sc.is_attached()) { + m_sync_sc.async_set_window(sc_current_window); + } else { + SCP_FATAL(()) << "Window must be attached before calling async_set_window()"; + } + m_num_vcpus = m_inst.plugin_api().qemu_plugin_num_vcpus(); + m_sync_sc.register_sync_cb(std::bind(&McipsPlugin::receive_window_cb, this, std::placeholders::_1)); + + enable_vcpu_tb_trans(m_id, &bridge_vcpu_tb_trans); + enable_vcpu_init(m_id, &bridge_vcpu_init); + enable_vcpu_resume(m_id, &bridge_vcpu_resume); + enable_vcpu_idle(m_id, &bridge_vcpu_idle); + /* USERDATA-style: register directly with the plugin API; the static + * bridge below uses LibQemuPlugin::dispatch_userdata for shutdown + * synchronization against the leaked PluginHandle. */ + m_inst.plugin_api().qemu_plugin_register_time_cb(m_time_handle, &bridge_get_qemu_clock, handle_as_userdata()); + } + + /** @brief Helper function to get vCPU time structure from scoreboard */ + vCPUTime* get_vcpu(unsigned int cpu_index) + { + return reinterpret_cast( + m_inst.plugin_api().qemu_plugin_scoreboard_find(m_vcpus_scoreboard, cpu_index)); + } + + void attach_sync_window() + { + if (!m_sync_sc.is_attached()) m_sync_sc.attach(); + } + + void detach_sync_window() + { + if (m_sync_sc.is_attached()) m_sync_sc.detach(m_qemu_time); + } + + /** + * @brief Set the instructions per second for a specific vCPU + * @param cpu_index Index of the vCPU + * @param insn_per_second Instructions per second value to set + * @return true if successful, false if vCPU not found in scoreboard + */ + bool set_vcpu_insn_per_second(unsigned int cpu_index, uint64_t insn_per_second) + { + if (insn_per_second == 0) { + SCP_FATAL(()) << "insn_per_second must be > 0 (cpu_" << cpu_index << ")"; + return false; + } + if (cpu_index >= static_cast(m_num_vcpus)) { + return false; + } + + vCPUTime* vcpu = get_vcpu(cpu_index); + if (!vcpu) { + return false; + } + + vcpu->insn_per_second = insn_per_second; + return true; + } + + /** @brief Copy m_qemu_time into the atomic nanosecond field. Must hold m_mcips_mutex when calling. */ + void sync_qemu_time_ns() + { + m_qemu_time_ns.store(static_cast(m_qemu_time.to_seconds() * NSEC_IN_ONE_SEC), + std::memory_order_release); + } + + /** @brief Time represented by the in-flight instructions in delta_insn. */ + static sc_core::sc_time cpu_delta_time(const vCPUTime* vcpu) + { + sc_assert(vcpu && "cpu_delta_time called with null vCPU"); + return sc_core::sc_time(static_cast(vcpu->delta_insn) / vcpu->insn_per_second, sc_core::SC_SEC); + } + + /** @brief Current time for a specific CPU (base + in-flight delta). Must hold m_mcips_mutex when calling. */ + sc_core::sc_time cpu_time_now(const vCPUTime* vcpu) + { + sc_assert(vcpu && "cpu_time_now called with null vCPU"); + if (vcpu->cpu_execution_status == IDLE) { + return vcpu->cpu_time; + } + return vcpu->cpu_time + cpu_delta_time(vcpu); + } + + /** + * @brief Current QEMU time (base + active CPU's in-flight delta). + * + * When called under m_mcips_mutex, prefer the overload that takes an + * explicit @p active pointer to avoid a redundant atomic load. + */ + sc_core::sc_time qemu_time_now() { return qemu_time_now(m_active_vcpu.load(std::memory_order_acquire)); } + + /** @brief Overload for callers that already hold m_mcips_mutex and have the active pointer. */ + sc_core::sc_time qemu_time_now(const vCPUTime* active) + { + if (!active) return m_qemu_time; + return m_qemu_time + cpu_delta_time(active); + } + + /** @brief Find the CPU that is furthest behind in time (skips idle and halted ones). */ + vCPUTime* slowest_active_cpu() + { + vCPUTime* slowest_cpu = nullptr; + sc_core::sc_time min_time = sc_core::SC_ZERO_TIME; + + for (int i = 0; i < m_num_vcpus; i++) { + auto* vcpu = get_vcpu(i); + if (vcpu->cpu_execution_status == IDLE) continue; + + const sc_core::sc_time current_cpu_time = cpu_time_now(vcpu); + if (!slowest_cpu || current_cpu_time < min_time) { + min_time = current_cpu_time; + slowest_cpu = vcpu; + } + } + return slowest_cpu; + } + + /** + * @brief Pick the first non-idle, non-halted CPU and make it the active one. + * + * @return The chosen CPU, or nullptr if every CPU is idle or halted. + */ + vCPUTime* select_active_vcpu() + { + for (int i = 0; i < m_num_vcpus; i++) { + vCPUTime* vcpu = get_vcpu(i); + if (vcpu->cpu_execution_status != IDLE) { + m_active_vcpu.store(vcpu, std::memory_order_release); + SCP_DEBUG(()) << "cpu_" << vcpu->index << " is the new active cpu"; + return vcpu; + } + } + m_active_vcpu.store(nullptr, std::memory_order_release); + return nullptr; + } + + /** @brief Pause the CPU if it has run too far ahead of the allowed limit. m_mcips_mutex must be held. */ + void pause_if_ahead(vCPUTime* vcpu, const vCPUTime* active, sc_core::sc_time current_qemu_time, + sc_core::sc_time threshold) + { + if (vcpu->cpu_execution_status != RUNNING) return; + + const sc_core::sc_time vcpu_time = cpu_time_now(vcpu); + if ((vcpu == active && current_qemu_time > threshold) || (vcpu != active && vcpu_time > current_qemu_time)) { + vcpu->cpu_execution_status = PAUSED; + m_inst.plugin_api().qemu_plugin_cpu_request_pause(static_cast(vcpu->index)); + SCP_DEBUG(()) << "cpu_" << vcpu->index << " paused, time=" << vcpu_time; + } + } + + /** @brief Resume paused CPUs that are still within the allowed time limit. m_mcips_mutex must be held. */ + void resume_if_behind(const vCPUTime* active, sc_core::sc_time current_qemu_time, sc_core::sc_time threshold) + { + for (int i = 0; i < m_num_vcpus; i++) { + auto* vcpu = get_vcpu(i); + if (vcpu->cpu_execution_status != PAUSED) continue; + const sc_core::sc_time vcpu_time = cpu_time_now(vcpu); + if ((vcpu == active && current_qemu_time <= threshold) || + (vcpu != active && vcpu_time <= current_qemu_time)) { + vcpu->cpu_execution_status = RUNNING; + m_inst.plugin_api().qemu_plugin_cpu_resume(static_cast(vcpu->index)); + SCP_DEBUG(()) << "cpu_" << vcpu->index << " resumed, time=" << vcpu_time; + } + } + } + + /** + * @brief Compute the time threshold that determines which CPUs may run. + * + * The threshold is the lesser of (a) the end of the current SystemC window + * and (b) the slowest active CPU's time plus one quantum. + * m_mcips_mutex must be held. + * + * @return The threshold, or SC_ZERO_TIME if there are no active CPUs. + */ + sc_core::sc_time compute_threshold() + { + const vCPUTime* slowest_cpu = slowest_active_cpu(); + if (!slowest_cpu) return sc_core::SC_ZERO_TIME; + return std::min(sc_current_window.to, cpu_time_now(slowest_cpu) + m_quantum); + } + + /** + * @brief Rebalance CPUs and notify SystemC. m_mcips_mutex must be held. + * + * Computes the current threshold, pauses @p vcpu if it is ahead, + * resumes any paused CPUs that are behind, and sends a new window. + * If @p vcpu is nullptr only resume + window are performed. + */ + void rebalance_and_sync(vCPUTime* vcpu = nullptr) + { + const auto* active = m_active_vcpu.load(std::memory_order_relaxed); + + if (!active) { + m_all_cpus_idle.store(true, std::memory_order_release); + SCP_DEBUG(()) << "[idle_pump] rebalance_and_sync(no-active): all-idle path"; + detach_sync_window(); + return; + } + + SCP_DEBUG(()) << "rebalance_and_sync: active cpu present"; + const sc_core::sc_time threshold = compute_threshold(); + if (threshold == sc_core::SC_ZERO_TIME) { + SCP_DEBUG(()) << "rebalance_and_sync: threshold=0 with active cpu, skipping"; + return; + } + + const sc_core::sc_time current_qemu_time = qemu_time_now(active); + if (vcpu) { + pause_if_ahead(vcpu, active, current_qemu_time, threshold); + } + resume_if_behind(active, current_qemu_time, threshold); + set_systemc_window(); + } + + /** + * @brief Set the SystemC synchronization window + * @param custom_window Optional custom window to set; if nullptr, calculates window from current QEMU time + */ + void set_systemc_window( + const sc_core::sc_sync_window::window* custom_window = nullptr) + { + if (m_sync_sc.is_attached()) { + if (custom_window) { + SCP_DEBUG(()) << "set_systemc_window::custom_window.from= " << custom_window->from + << ", custom_window.to= " << custom_window->to; + m_sync_sc.async_set_window(*custom_window); + } else { + sc_core::sc_time current_qemu_time = qemu_time_now(); + SCP_DEBUG(()) << "set_systemc_window::qemu_cpu_time_now = " << current_qemu_time + << ", sc_current_window.from= " << sc_current_window.from + << ", sc_current_window.to= " << sc_current_window.to; + m_sync_sc.async_set_window({ current_qemu_time, (current_qemu_time + m_quantum) }); + } + } else { + SCP_INFO(()) << "Window must be attached before calling set_systemc_window()"; + } + } + + /** + * @brief Called by SystemC when a new time window is ready. + * + * This runs on the SystemC thread. It only takes the m_mcips_mutex (not BQL). + * Saves the new window, detaches if no CPU is active, resumes the active + * CPU if the new window gives it room, then sends a new window back. + */ + void receive_window_cb(const sc_core::sc_sync_window::window& sc_w) + { + if (m_shutdown.load(std::memory_order_seq_cst)) return; + + m_inflight_cb.fetch_add(1, std::memory_order_seq_cst); + if (m_shutdown.load(std::memory_order_seq_cst)) { + m_inflight_cb.fetch_sub(1, std::memory_order_seq_cst); + return; + } + + std::lock_guard lock(m_mcips_mutex); + sc_current_window = sc_w; + + auto* active = m_active_vcpu.load(std::memory_order_relaxed); + SCP_DEBUG(()) << "receive_window_cb: qemu_time=" << qemu_time_now(active) << ", sc_window=[" + << sc_current_window.from << ", " << sc_current_window.to << "]"; + + if (!active) { + SCP_DEBUG(()) << "[idle_pump] receive_window_cb(no-active): calling rebalance_and_sync"; + rebalance_and_sync(); + m_inflight_cb.fetch_sub(1, std::memory_order_seq_cst); + return; + } + + if (active->cpu_execution_status == PAUSED) { + const sc_core::sc_time threshold = compute_threshold(); + if (threshold != sc_core::SC_ZERO_TIME && qemu_time_now(active) <= threshold) { + active->cpu_execution_status = RUNNING; + m_inst.plugin_api().qemu_plugin_cpu_resume(static_cast(active->index)); + SCP_DEBUG(()) << "receive_window_cb: resumed active cpu"; + } + } + + set_systemc_window(); + m_inflight_cb.fetch_sub(1, std::memory_order_seq_cst); + } + + /** @brief Called under BQL before simulation starts; does not hold m_mcips_mutex. */ + void vcpu_init(qemu_plugin_id_t id, unsigned int cpu_index) override + { + if (m_shutdown.load(std::memory_order_acquire)) return; + + const int current = m_inst.plugin_api().qemu_plugin_num_vcpus(); + if (current > m_num_vcpus) m_num_vcpus = current; + + vCPUTime* vcpu = get_vcpu(cpu_index); + vcpu->index = cpu_index; + vcpu->insn_per_second = 1'000'000'000; + vcpu->delta_insn = 0; + vcpu->cpu_time = sc_core::SC_ZERO_TIME; + vcpu->cpu_execution_status = RUNNING; + + if (m_active_vcpu.load(std::memory_order_relaxed) == nullptr) { + m_active_vcpu.store(vcpu, std::memory_order_release); + } + + m_first_vcpu_initialized.store(true, std::memory_order_release); + } + + /** + * @brief Called when a CPU has run its full instruction quota. + * Adds the time used, pauses/resumes CPUs as needed, and sends a new window. + * Takes and releases m_mcips_mutex inside. + */ + void cpu_end_delta_quota(vCPUTime* vcpu) + { + std::lock_guard lock(m_mcips_mutex); + + if (vcpu->cpu_execution_status != RUNNING) { + return; + } + + const sc_core::sc_time delta_time = cpu_delta_time(vcpu); + const auto* active = m_active_vcpu.load(std::memory_order_relaxed); + sc_assert(active && "cpu_end_delta_quota called but no active CPU"); + + const sc_core::sc_time old_qemu_time = m_qemu_time; + if (active == vcpu) { + m_qemu_time += delta_time; + sync_qemu_time_ns(); + SCP_DEBUG(()) << "cpu_" << vcpu->index << " completed quantum, qemu_time: " << old_qemu_time << " -> " + << m_qemu_time << ", delta=" << delta_time << ", sc_time=" << sc_core::sc_time_stamp(); + } + vcpu->cpu_time += delta_time; + vcpu->delta_insn = 0; + + rebalance_and_sync(vcpu); + } + + /** @brief Called when a CPU has run at least global_quantum instructions. */ + void vcpu_tb_exec_cond(unsigned int cpu_index, void* /*udata*/) + { + if (m_shutdown.load(std::memory_order_acquire)) return; + + vCPUTime* vcpu = get_vcpu(cpu_index); + if (vcpu->cpu_execution_status != RUNNING) { + return; + } + + sc_assert(vcpu->delta_insn >= m_global_quantum && "TB exec condition fired but quota not met"); + cpu_end_delta_quota(vcpu); + } + + /** + * @brief Static bridge for vcpu_tb_exec_cond — registered directly with QEMU. + * Uses the leaked PluginHandle (handle_as_userdata) so post-destruction + * callbacks see alive==false and return without touching freed memory. + */ + static void bridge_vcpu_tb_exec_cond(unsigned int cpu_index, void* userdata) + { + LibQemuPlugin::dispatch_userdata( + userdata, [&](LibQemuPlugin* p) { static_cast(p)->vcpu_tb_exec_cond(cpu_index, userdata); }); + } + + /** @brief Called when QEMU translates a block, adds instruction counting and quota check. */ + void vcpu_tb_trans(qemu_plugin_id_t id, qemu_plugin_tb* tb) override + { + const size_t n_insns = m_inst.plugin_api().qemu_plugin_tb_n_insns(tb); + qemu_plugin_u64 delta_insn = qemu_plugin_scoreboard_u64_in_struct(m_vcpus_scoreboard, vCPUTime, delta_insn); + + m_inst.plugin_api().qemu_plugin_register_vcpu_tb_exec_inline_per_vcpu(tb, QEMU_PLUGIN_INLINE_ADD_U64, + delta_insn, n_insns); + + m_inst.plugin_api().qemu_plugin_register_vcpu_tb_exec_cond_cb( + tb, &bridge_vcpu_tb_exec_cond, QEMU_PLUGIN_CB_NO_REGS, QEMU_PLUGIN_COND_GE, delta_insn, m_global_quantum, + handle_as_userdata()); + } + + /** + * @brief Called when a CPU goes idle (QEMU holds BQL; we additionally take m_mcips_mutex). + * + * RUNNING → IDLE: save the time used, pick a new active CPU or start the idle pump. + * Takes m_mcips_mutex to protect shared McipsPlugin state. + */ + void vcpu_idle(qemu_plugin_id_t id, unsigned int cpu_index) override + { + if (m_shutdown.load(std::memory_order_acquire)) return; + + std::lock_guard lock(m_mcips_mutex); + SCP_DEBUG(()) << "vcpu_idle callback for cpu_" << cpu_index; + vCPUTime* vcpu = get_vcpu(cpu_index); + + switch (vcpu->cpu_execution_status) { + case RUNNING: { + SCP_DEBUG(()) << "cpu_" << cpu_index << " RUNNING → IDLE"; + vcpu->cpu_execution_status = IDLE; + + const auto* active = m_active_vcpu.load(std::memory_order_relaxed); + if (vcpu == active) { + SCP_DEBUG(()) << "cpu_" << cpu_index << " was active, selecting new active cpu"; + + const sc_core::sc_time delta = cpu_delta_time(vcpu); + m_qemu_time += delta; + sync_qemu_time_ns(); + vcpu->cpu_time += delta; + + vCPUTime* new_active = select_active_vcpu(); + + if (!new_active) { + m_all_cpus_idle.store(true, std::memory_order_release); + if (!m_shutdown.load(std::memory_order_acquire)) { + m_idle_tick.notify(m_quantum); + SCP_DEBUG(()) << "[idle_pump] NOTIFY from vcpu_idle (cpu_" << cpu_index << ")"; + } + detach_sync_window(); + } else { + new_active->cpu_time = m_qemu_time; + new_active->delta_insn = 0; + + if (new_active->cpu_execution_status == PAUSED) { + new_active->cpu_execution_status = RUNNING; + m_inst.plugin_api().qemu_plugin_cpu_resume(static_cast(new_active->index)); + } + } + } + vcpu->delta_insn = 0; + rebalance_and_sync(); + break; + } + case PAUSED: + SCP_DEBUG(()) << "vcpu_idle: cpu_" << cpu_index << " already PAUSED"; + break; + + case IDLE: + SCP_DEBUG(()) << "vcpu_idle: cpu_" << cpu_index << " already IDLE"; + break; + + default: + SCP_FATAL(()) << "vcpu_idle: invalid execution status for cpu_" << cpu_index; + sc_assert(false); + break; + } + } + + /** + * @brief Called when a CPU wakes up (QEMU holds BQL; we additionally take m_mcips_mutex). + * + * IDLE → RUNNING: skip if halted; first CPU to wake re-attaches the sync window. + * PAUSED → RUNNING: only if the CPU is still within the allowed time range. + * RUNNING: nothing to do (plugin already set it to RUNNING earlier). + * Takes m_mcips_mutex to protect shared McipsPlugin state. + */ + void vcpu_resume(qemu_plugin_id_t id, unsigned int cpu_index) override + { + if (m_shutdown.load(std::memory_order_acquire)) return; + + std::lock_guard lock(m_mcips_mutex); + SCP_DEBUG(()) << "vcpu_resume callback for cpu_" << cpu_index; + vCPUTime* vcpu = get_vcpu(cpu_index); + + switch (vcpu->cpu_execution_status) { + case IDLE: { + SCP_DEBUG(()) << "cpu_" << cpu_index << " IDLE → RUNNING"; + vcpu->cpu_execution_status = RUNNING; + vcpu->cpu_time = qemu_time_now(); + + if (m_active_vcpu.load(std::memory_order_relaxed) == nullptr) { + const bool was_pumping = m_all_cpus_idle.exchange(false, std::memory_order_release); + + const sc_core::sc_time sc_now = sc_core::sc_time(sc_core::sc_time_stamp().to_seconds(), + sc_core::SC_SEC); + if (sc_now > m_qemu_time) { + m_qemu_time = sc_now; + sync_qemu_time_ns(); + } + + vcpu->cpu_time = m_qemu_time; + m_active_vcpu.store(vcpu, std::memory_order_release); + + attach_sync_window(); + set_systemc_window(); + SCP_DEBUG(()) << "[idle_pump] STOP vcpu_resume: cpu_" << cpu_index + << " first to resume, was_pumping=" << was_pumping; + } + break; + } + case PAUSED: { + const auto* active = m_active_vcpu.load(std::memory_order_relaxed); + const sc_core::sc_time threshold = compute_threshold(); + const sc_core::sc_time current_qemu_time = qemu_time_now(active); + const sc_core::sc_time vcpu_time = cpu_time_now(vcpu); + + if (threshold != sc_core::SC_ZERO_TIME && ((vcpu == active && current_qemu_time <= threshold) || + (vcpu != active && vcpu_time <= current_qemu_time))) { + vcpu->cpu_execution_status = RUNNING; + m_inst.plugin_api().qemu_plugin_cpu_resume(static_cast(vcpu->index)); + SCP_DEBUG(()) << "vcpu_resume: cpu_" << vcpu->index << " PAUSED → RUNNING"; + } else { + m_inst.plugin_api().qemu_plugin_cpu_request_pause(static_cast(vcpu->index)); + SCP_DEBUG(()) << "vcpu_resume: cpu_" << vcpu->index << " CAN NOT BE RESUMED"; + } + break; + } + case RUNNING: + SCP_DEBUG(()) << "vcpu_resume: cpu_" << cpu_index << " already RUNNING"; + break; + + default: + SCP_FATAL(()) << "vcpu_resume: invalid execution status for cpu_" << cpu_index; + sc_assert(false); + break; + } + } + + /** + * @brief Returns the current simulation time in nanoseconds to QEMU's timer thread. + * + * This runs without holding m_mcips_mutex or BQL. When idle, it follows + * SystemC time and schedules idle ticks so timers keep working. + * + * @note delta_insn is read without m_mcips_mutex but it is updated atomically + * by QEMU, so reading it from another thread is safe. insn_per_second is set + * once during initialization and never changes at runtime, so reading it + * without the mutex is also safe. + */ + int64_t get_qemu_clock(void* /*userdata*/) + { + if (m_shutdown.load(std::memory_order_acquire)) { + return static_cast(sc_core::sc_time_stamp().to_seconds() * NSEC_IN_ONE_SEC); + } + + int64_t qemu_time = static_cast(qemu_time_now().to_seconds() * NSEC_IN_ONE_SEC); + + auto active = m_active_vcpu.load(std::memory_order_relaxed); + if (!active) { + // Cannot use SCP logging here — runs from a QEMU thread outside the SystemC hierarchy. + // Clamp qemu_time to SystemC time to prevent drift at startup. + if (qemu_time > 0) { + int64_t systemc_time_now = static_cast(sc_core::sc_time_stamp().to_seconds() * + NSEC_IN_ONE_SEC); + if (systemc_time_now > qemu_time) { + qemu_time = systemc_time_now; + } + } + + if (!m_first_vcpu_initialized.load(std::memory_order_acquire)) { + m_idle_tick.notify(m_quantum); + } + } else { + // Watchdog: detect iothread livelock (qemu_time frozen >1s wall) and async-notify + // m_idle_tick. notify() from this iothread routes via async_request_update so it + // wakes the SC kernel even out of sc_prim_channel_registry::async_suspend. + int64_t prev = m_wd_last_clock_ns.load(std::memory_order_relaxed); + if (prev != qemu_time) { + m_wd_last_clock_ns.store(qemu_time, std::memory_order_relaxed); + m_wd_freeze_start_ns.store(0, std::memory_order_relaxed); + } else { + const int64_t now_ns = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + int64_t start = m_wd_freeze_start_ns.load(std::memory_order_relaxed); + if (start == 0) { + m_wd_freeze_start_ns.compare_exchange_strong(start, now_ns, std::memory_order_relaxed); + } else if (now_ns - start > kWdFreezeThresholdNs) { + m_idle_tick.notify(sc_core::SC_ZERO_TIME); + m_wd_freeze_start_ns.store(now_ns, std::memory_order_relaxed); // throttle re-kicks + } + } + } + + return qemu_time; + } + + /** + * @brief Static bridge for the time callback — registered directly with QEMU. + * Returns SystemC time as a fallback when the plugin handle is dead, so + * callbacks arriving after destruction don't deref freed memory. + */ + static int64_t bridge_get_qemu_clock(void* userdata) + { + int64_t result = static_cast(sc_core::sc_time_stamp().to_seconds() * NSEC_IN_ONE_SEC); + LibQemuPlugin::dispatch_userdata( + userdata, [&](LibQemuPlugin* p) { result = static_cast(p)->get_qemu_clock(nullptr); }); + return result; + } + + /** + * @brief Returns a JSON string with the current state of all CPUs (for debugging). + * Reads without m_mcips_mutex so values may be slightly out of date. + */ + std::string get_mcips_status_json() + { + auto active = m_active_vcpu.load(std::memory_order_relaxed); + std::ostringstream os; + + const uint64_t qemu_time = static_cast(qemu_time_now().to_seconds() * NSEC_IN_ONE_SEC); + + os << "{" << "\"name\":\"" << name() << "\"," << "\"qemu_time\":\"" << qemu_time << " ns\"," + << "\"n_cpus\":" << m_num_vcpus << "," + << "\"active_vcpu_index\":" << (active ? static_cast(active->index) : -1) << "," << "\"vcpus\":["; + + bool first = true; + for (int i = 0; i < m_num_vcpus; i++) { + auto* vcpu = get_vcpu(i); + if (!vcpu) continue; + + if (!first) os << ","; + first = false; + + const uint64_t ns = static_cast(vcpu->cpu_time.to_seconds() * NSEC_IN_ONE_SEC); + + os << "{" << "\"index\":" << vcpu->index << "," << "\"insn_per_second\":\"" << vcpu->insn_per_second + << "\"," << "\"delta_insn\":\"" << vcpu->delta_insn << "\"," << "\"cpu_time_ns\":\"" << ns << "\"," + << "\"cpu_execution_status\":" << static_cast(vcpu->cpu_execution_status) << "}"; + } + os << "]}"; + + return os.str(); + } +}; + +#endif //_LIBQBOX_COMPONENTS_MCIPS_PLUGIN_H diff --git a/qemu-components/common/include/qemu-instance.h b/qemu-components/common/include/qemu-instance.h index 822eac58..1fc6db7c 100644 --- a/qemu-components/common/include/qemu-instance.h +++ b/qemu-components/common/include/qemu-instance.h @@ -28,6 +28,7 @@ #include #include "ports/qemu-target-signal-socket.h" +#include "mcips-plugin.h" class QemuDeviceBaseIF { @@ -147,6 +148,9 @@ class QemuInstance : public sc_core::sc_module } protected: + cci::cci_param p_enable_mcips_plugin; + std::unique_ptr m_mcips_plugin; + qemu::LibQemu m_inst; QemuInstanceDmiManager m_dmi_mgr; @@ -310,6 +314,10 @@ class QemuInstance : public sc_core::sc_module , p_args("qemu_args", "", "additional space separated arguments") , p_accel("accel", "tcg", "Virtualization accelerator") , p_whpx_args("whpx_args", "", "Additional WHPX accelerator properties (e.g. gicd-base-address=0x17000000)") + , p_enable_mcips_plugin("enable_mcips_plugin", false, + "If true, enable multi core instructions per second plugin (mcips_plugin) to calculate " + "and sync time between QEMU and SystemC based on the number of " + "instructions.") { SCP_DEBUG(()) << "Libqbox QemuInstance constructor"; @@ -319,11 +327,29 @@ class QemuInstance : public sc_core::sc_module m_running = true; p_tcg_mode.lock(); push_default_args(); + if (p_enable_mcips_plugin) { + m_mcips_plugin = std::make_unique("mcips_plugin", m_inst); + /* Plugin filename per platform: meson shared_module() defaults */ +#if defined(_WIN32) + m_mcips_plugin->push_plugin_args("libidlinker.dll"); +#elif defined(__APPLE__) + m_mcips_plugin->push_plugin_args("libidlinker.dylib"); +#else /* Linux */ + m_mcips_plugin->push_plugin_args("libidlinker.so"); +#endif + } } QemuInstance(const QemuInstance&) = delete; QemuInstance(QemuInstance&&) = delete; - virtual ~QemuInstance() { m_running = false; } + + virtual ~QemuInstance() + { + m_running = false; + if (m_mcips_plugin) { + m_mcips_plugin.reset(); + } + } bool operator==(const QemuInstance& b) const { return this == &b; } @@ -488,6 +514,24 @@ class QemuInstance : public sc_core::sc_module return m_inst; } + /** + * @brief Returns the mcips_plugin. + * + * @details Returns the mcips_plugin for the current instance. + */ + McipsPlugin& get_mcips_plugin() + { + sc_assert(m_mcips_plugin); + return *m_mcips_plugin; + } + + /** + * @brief Check if multi core instructions per second plugin (mcips) is enabled. + * + * @details Returns true only if the CCI parameter requested it AND the plugin was actually constructed. + */ + bool is_mcips_enabled() const noexcept { return p_enable_mcips_plugin && static_cast(m_mcips_plugin); } + /** * @brief Returns the locked QemuInstanceDmiManager instance * @@ -500,6 +544,12 @@ class QemuInstance : public sc_core::sc_module int number_devices() { return devices.size(); } private: + void before_end_of_elaboration() + { + if (!is_inited()) { + init(); // dlsyms libqemu_init; plugin functions are part of the returned LibQemuExports table. + } + } void start_of_simulation(void) { get().finish_qemu_init(); } void reset_cb(const bool val) diff --git a/qemu-components/common/src/libqemu-cxx/libqemu-cxx.cc b/qemu-components/common/src/libqemu-cxx/libqemu-cxx.cc index f9196e8e..0d934b31 100644 --- a/qemu-components/common/src/libqemu-cxx/libqemu-cxx.cc +++ b/qemu-components/common/src/libqemu-cxx/libqemu-cxx.cc @@ -91,6 +91,8 @@ void LibQemu::init() init_callbacks(); } +const LibQemuExports& LibQemu::plugin_api() const { return m_int->exports(); } + void LibQemu::start_gdb_server(std::string port) { m_int->exports().gdbserver_start(port.c_str()); diff --git a/qemu-components/common/src/libqemu-cxx/libqemu-plugin.cc b/qemu-components/common/src/libqemu-cxx/libqemu-plugin.cc new file mode 100644 index 00000000..5fc787aa --- /dev/null +++ b/qemu-components/common/src/libqemu-cxx/libqemu-plugin.cc @@ -0,0 +1,8 @@ +/* + * This file is part of libqemu-cxx + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. All Rights Reserved. + * + * SPDX-License-Identifier: BSD-3-Clause-Clear + */ + +#include diff --git a/systemc-components/common/include/runonsysc.h b/systemc-components/common/include/runonsysc.h index dce82992..5618d6bf 100644 --- a/systemc-components/common/include/runonsysc.h +++ b/systemc-components/common/include/runonsysc.h @@ -115,6 +115,9 @@ class runonsysc : public sc_core::sc_module sc_core::sc_unsuspendable(); (*core->running_job)(); + /* Drain any notify(SC_ZERO_TIME) events the job queued + * (e.g. deferred IRQ SC_METHODs) before allowing suspend. */ + wait(sc_core::SC_ZERO_TIME); sc_core::sc_suspendable(); lock.lock(); diff --git a/systemc-components/common/include/sync_window.h b/systemc-components/common/include/sync_window.h index b1b5f7e0..d6513c75 100644 --- a/systemc-components/common/include/sync_window.h +++ b/systemc-components/common/include/sync_window.h @@ -73,6 +73,7 @@ class sc_sync_window : public sc_core::sc_module, public sc_core::sc_prim_channe window m_window; window m_incomming_window; // used to hold the window values coming in from // the other side. + bool m_is_attached; // Track attachment state std::function m_other_async_set_window_fn; @@ -155,16 +156,49 @@ class sc_sync_window : public sc_core::sc_module, public sc_core::sc_prim_channe { /* Only accept updated windows so we dont re-send redundant updates */ std::lock_guard lg(m_mutex); + // Check if window is attached before allowing window updates + if (!m_is_attached) { + SCP_FATAL(()) << "Cannot set window on detached sync_window. " + << "Window must be attached before calling async_set_window(). "; + } m_incomming_window = w; if (!(w == m_window)) { SCP_INFO(()) << "sync " << w.from << " " << w.to; async_request_update(); } } - void detach() + /** + * @brief Check if the sync_window is currently attached. + * @return true if attached, false if detached + */ + bool is_attached() const { return m_is_attached; } + /** + * @brief Attach to the primitive channel's suspend/resume mechanism for time synchronization. + * + * When attached, this sync_window actively participates in SystemC's suspend/resume protocol, + * allowing it to suspend simulation when synchronization windows are reached. + */ + void attach() + { + async_attach_suspending(); + m_is_attached = true; + } + /** + * @brief Detach from the primitive channel's suspend/resume mechanism and fully open the time window. + * + * When detached, this sync_window no longer participates in suspend/resume, meaning it cannot + * block simulation time advancement. Additionally, the synchronization window is set to + * [now, sc_max_time()], effectively removing all time restrictions and allowing the simulation + * to advance freely without waiting for the paired sync_window. + * + * @param now Starting time for the unrestricted window (defaults to SC_ZERO_TIME) + */ + void detach(const sc_core::sc_time now = sc_core::SC_ZERO_TIME) { + async_set_window({ now, sc_core::sc_max_time() }); // setting open window before detaching, since + // async_set_window required an attached window. async_detach_suspending(); - m_other_async_set_window_fn(open_window); + m_is_attached = false; } void bind(sc_sync_window* other) { @@ -184,7 +218,7 @@ class sc_sync_window : public sc_core::sc_module, public sc_core::sc_prim_channe } m_other_async_set_window_fn = fn; } - SC_CTOR (sc_sync_window) : m_window({sc_core::SC_ZERO_TIME, policy.quantum()}) + SC_CTOR (sc_sync_window) : m_window({sc_core::SC_ZERO_TIME, policy.quantum()}), m_is_attached(false) { SCP_TRACE(())("Constructor"); SC_METHOD(sweep_helper); @@ -197,7 +231,7 @@ class sc_sync_window : public sc_core::sc_module, public sc_core::sc_prim_channe m_step_ev.notify(sc_core::SC_ZERO_TIME); - this->sc_core::sc_prim_channel::async_attach_suspending(); + this->attach(); // This will set m_is_attached to true } }; diff --git a/systemc-components/common/src/dynlib_loader.cc b/systemc-components/common/src/dynlib_loader.cc index f3688878..4083a42a 100644 --- a/systemc-components/common/src/dynlib_loader.cc +++ b/systemc-components/common/src/dynlib_loader.cc @@ -19,6 +19,25 @@ #include #include +#include + +extern "C" { +/* + * This function sets the CCI parameter. It gets the broker and parameter handle, + * then sets the value if the handle is valid, or creates a new parameter if it is not. + */ +void global_set_cci_param(char* key, uint64_t val) +{ + auto b = cci::cci_get_broker(); + auto b_key = b.get_param_handle(key); + + if (b_key.is_valid()) { + b_key.set_cci_value(cci::cci_value(val)); + } else { + b.set_preset_cci_value(key, cci::cci_value(val)); + } +} +} #if defined(_WIN32) #include diff --git a/systemc-components/monitor/include/monitor.h b/systemc-components/monitor/include/monitor.h index 28478998..06023777 100644 --- a/systemc-components/monitor/include/monitor.h +++ b/systemc-components/monitor/include/monitor.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -108,12 +109,14 @@ class monitor : public sc_core::sc_module cci::cci_param p_html_doc_template_dir_path; cci::cci_param p_html_doc_name; cci::cci_param p_use_html_presentation; + cci::cci_param p_refresh_interval_ms; private: crow::SimpleApp m_app; std::future m_app_future; gs::runonsysc m_sc; std::vector m_qks; + std::vector m_mcips_plugins; }; } // namespace gs diff --git a/systemc-components/monitor/src/monitor.cc b/systemc-components/monitor/src/monitor.cc index c2e94fac..84ec8d51 100644 --- a/systemc-components/monitor/src/monitor.cc +++ b/systemc-components/monitor/src/monitor.cc @@ -16,6 +16,7 @@ platform["monitor_0"] = { use_html_presentation = true; html_doc_template_dir_path = "/path/to/html/templates"; html_doc_name = "monitor.html"; + refresh_interval_ms = 100; }; */ @@ -83,6 +84,7 @@ monitor::monitor(const sc_core::sc_module_name& nm) "path to a template directory where HTML document to call the REST API exist") , p_html_doc_name("html_doc_name", "monitor.html", "name of a HTML document to call the REST API") , p_use_html_presentation("use_html_presentation", true, "use HTML document to present the REST API") + , p_refresh_interval_ms("refresh_interval_ms", 100, "refresh interval in milliseconds for the web interface") { SCP_DEBUG(()) << "monitor constructor"; m_app.signal_clear(); @@ -248,7 +250,8 @@ void monitor::init_monitor() return page.render_string(); } else { std::string ret = - "API:\n/sc_time\n/pause\n/continue\n/reset\n/object/\n//object/\n/qk_status\n/sc_suspended\n/" + "API:\n/sc_time\n/pause\n/continue\n/reset\n/object/\n//object/\nmcips_plugin_status\n/" + "qk_status\n/sc_suspended\n/refresh_interval\n/" "transport_dbg//"; return ret; } @@ -325,12 +328,33 @@ void monitor::init_monitor() crow::json::wvalue r = crow::json::wvalue::list(cr); return r; }); + CROW_ROUTE(m_app, "/mcips_plugin_status") + ([&]() { + std::ostringstream os; + os << "["; + bool firstPlugin = true; + for (auto* p : m_mcips_plugins) { + if (!firstPlugin) os << ","; + firstPlugin = false; + os << p->get_mcips_status_json(); // returns a JSON string per plugin + } + os << "]"; + crow::response res(os.str()); + res.add_header("Content-Type", "application/json"); + return res; + }); CROW_ROUTE(m_app, "/sc_suspended") ([&]() { crow::json::wvalue r; r["sc_suspended"] = (sc_core::sc_get_status() == sc_core::SC_SUSPENDED); return r; }); + CROW_ROUTE(m_app, "/refresh_interval") + ([&]() { + crow::json::wvalue r; + r["refresh_interval_ms"] = p_refresh_interval_ms.get_value(); + return r; + }); CROW_ROUTE(m_app, "/transport_dbg//") ([&](uint64 addr, std::string name) { crow::json::wvalue r; @@ -416,6 +440,7 @@ template void monitor::end_of_elaboration() { m_qks = find_sc_objects(); + m_mcips_plugins = find_sc_objects(); } template diff --git a/systemc-components/monitor/static/monitor.html b/systemc-components/monitor/static/monitor.html index 3240194c..1456401d 100644 --- a/systemc-components/monitor/static/monitor.html +++ b/systemc-components/monitor/static/monitor.html @@ -133,6 +133,50 @@

VNC Servers List

+ + +

MCIPS Status

+ + + + + + + + + + + + + + +
PluginQEMU TimevCPU IndexInstructions/secDelta InstructionsCPU TimeStatus
+ +

QK Status

@@ -398,7 +442,6 @@

Registers

console.log("Error filtering servers:", error); } } - function prettyPrintServerInfo(serverInfo) { const vncInfoDiv = document.getElementById("vncInfo"); const prettyVncServerInfo = prettyPrintJson.toHtml(serverInfo); @@ -588,6 +631,9 @@

Registers

const qkdata = await qkresponse.json(); populateQKTable(qkdata); + const mcipsResp = await fetch('/mcips_plugin_status'); + populatemcipsTableMerged(await mcipsResp.json()); + const dropdown = document.getElementById('biflowsDropdown'); const selectedBiflow = dropdown.value; if ((!sock || sock.readyState > 1) && selectedBiflow) { @@ -615,6 +661,138 @@

Registers

const rsp = fetch('/pause'); } }); + + // ----- Constants & state ----- + const STATUS = ['IDLE','PAUSED', 'RUNNING']; + + const mcipsState = { + // key: `${plugin}:${vcpuIndex}` => last known status text + prevStatus: new Map(), + lastActive: new Map(), + }; + + function keyFor(plugin, idx) { return `${plugin}:${idx}`; } + + // Normalize active_vcpu_index + function normalizeActiveIndex(val, vcpuCount) { + const n = Number(val); + if (!Number.isFinite(n) || n < 0 || n >= vcpuCount || n > 1e12) return -1; + return n; + } + + // Convert time from nanoseconds to seconds or keep as nanoseconds based on checkbox + function formatTime(timeStr, useSeconds) { + if (!timeStr) return ''; + // Extract numeric value from string like "12345 ns" + const match = timeStr.match(/(\d+)/); + if (!match) return timeStr; + + const ns = Number(match[1]); + if (!Number.isFinite(ns)) return timeStr; + + if (useSeconds) { + const seconds = ns / 1e9; + return seconds.toFixed(9) + ' s'; + } else { + return ns + ' ns'; + } + } + + function addCell(tr, text, rowSpan, className) { + const td = tr.insertCell(); + if (rowSpan > 1) td.rowSpan = rowSpan; + if (className) td.className = className; + td.textContent = text ?? ''; + return td; + } + + // ----- populate with improved visibility ----- + function populatemcipsTableMerged(plugins) { + const tbody = document.querySelector('#mcipsTable tbody'); + if (!tbody) return; + + tbody.innerHTML = ''; + const frag = document.createDocumentFragment(); + + // Get checkbox state for time unit + const useSeconds = document.getElementById('timeUnitToggle')?.checked ?? true; + + (plugins || []).forEach(p => { + const pluginName = p.name || ''; + const vcpus = Array.isArray(p.vcpus) ? p.vcpus.slice() : []; + + // Stable order + vcpus.sort((a, b) => Number(a.index) - Number(b.index)); + + // Detect transitions + remember last RUNNING + vcpus.forEach(v => { + const idx = Number(v.index); + const cur = STATUS[Number(v.cpu_execution_status)] ?? 'UNKNOWN'; + const prev = mcipsState.prevStatus.get(keyFor(pluginName, idx)); + + if (cur === 'RUNNING') { + mcipsState.lastActive.set(pluginName, { + index: idx, + atScTime: p.qemu_time + }); + } + + mcipsState.prevStatus.set(keyFor(pluginName, idx), cur); + }); + + // Determine active and last active indices + const activeIdx = normalizeActiveIndex(p.active_vcpu_index, vcpus.length); + const allIdle = vcpus.length > 0 && vcpus.every(v => (STATUS[Number(v.cpu_execution_status)] ?? 'UNKNOWN') === 'IDLE'); + const last = mcipsState.lastActive.get(pluginName); + const lastActiveIndex = (allIdle && last && activeIdx === -1) ? Number(last.index) : null; + + // Draw block with rowSpan for plugin-level columns + const rowSpan = Math.max(vcpus.length, 1); + + for (let i = 0; i < rowSpan; i++) { + const v = vcpus[i] || {}; + const tr = document.createElement('tr'); + + // Plugin-level cells once + if (i === 0) { + addCell(tr, pluginName, rowSpan); + addCell(tr, formatTime(p.qemu_time, useSeconds), rowSpan, 'mono'); + } + + // vCPU Index with highlighting + const tdIdx = addCell(tr, v.index ?? '-', 1, 'num'); + const vCpuIdx = Number(v.index); + + // Highlight active vCPU in green + if (vCpuIdx === activeIdx && activeIdx >= 0) { + tdIdx.style.backgroundColor = '#90EE90'; // Light green + tdIdx.style.fontWeight = 'bold'; + } + // Highlight last active vCPU in red when all are idle + else if (lastActiveIndex !== null && vCpuIdx === lastActiveIndex) { + tdIdx.style.backgroundColor = '#FFB6C6'; // Light red + tdIdx.style.fontWeight = 'bold'; + } + + addCell(tr, v.insn_per_second ?? '0', 1, 'num mono'); + addCell(tr, v.delta_insn ?? '0', 1, 'num mono'); + addCell(tr, formatTime(v.cpu_time_ns, useSeconds), 1, 'mono'); + + const statusText = STATUS[Number(v.cpu_execution_status)] ?? 'UNKNOWN'; + const tdStatus = tr.insertCell(); + tdStatus.textContent = statusText; + tdStatus.style.color = + statusText === 'RUNNING' ? 'green' : + statusText === 'PAUSED' ? 'orange' : + 'gray'; + + frag.appendChild(tr); + } + }); + + tbody.appendChild(frag); + } + function populateQKTable(data) { const tableBody = document.getElementById('statusTable').getElementsByTagName('tbody')[0]; tableBody.innerHTML = ''; // Clear existing rows @@ -651,8 +829,35 @@

Registers

}); } - // Fetch the flag every 1 second - setInterval(fetchFlag, 1000); + // Fetch the refresh interval and start periodic updates + let refreshIntervalId; + + async function initializeRefreshInterval() { + try { + const response = await fetch('/refresh_interval'); + const data = await response.json(); + const intervalMs = data.refresh_interval_ms || 100; // fallback to 100ms + + // Clear any existing interval + if (refreshIntervalId) { + clearInterval(refreshIntervalId); + } + + // Start new interval with configured value + refreshIntervalId = setInterval(fetchFlag, intervalMs); + console.log(`Monitor refresh interval set to ${intervalMs}ms`); + } catch (error) { + console.error('Error fetching refresh interval, using default 100ms:', error); + // Fallback to default interval + if (refreshIntervalId) { + clearInterval(refreshIntervalId); + } + refreshIntervalId = setInterval(fetchFlag, 100); + } + } + + // Initialize refresh interval on page load + initializeRefreshInterval(); async function fetchObjects() { @@ -894,4 +1099,4 @@

Registers

- \ No newline at end of file + diff --git a/tests/qbox/CMakeLists.txt b/tests/qbox/CMakeLists.txt index a3c4eb58..cbce259f 100644 --- a/tests/qbox/CMakeLists.txt +++ b/tests/qbox/CMakeLists.txt @@ -87,6 +87,8 @@ function(qbox_add_cpu_test target timeout_s) ) endif() + #To disable MCIPS tests: pass -DQBOX_ENABLE_MCIPS_TESTS=OFF at configure time + option(QBOX_ENABLE_MCIPS_TESTS "Enable mcips=true test variants" ON) target_include_directories(${target} PRIVATE ${PROJECT_SOURCE_DIR}/tests/qbox/include ${keystone_SOURCE_DIR}/include) @@ -124,9 +126,16 @@ function(qbox_add_cpu_test target timeout_s) endif() endif() + # Only generate mcips=true when threading=MULTI and accel=tcg. + # For COROUTINE or SINGLE (or non-tcg), only mcips=false is generated. + set(mcips_variants false) + if(QBOX_ENABLE_MCIPS_TESTS AND "${threading}" STREQUAL "MULTI" AND "${accel}" STREQUAL "tcg") + set(mcips_variants true false) + endif() - foreach(num_cpu ${QBOX_CPU_TEST_NUM_CPU_COMBINATION}) - set(test_name ${target}:sync-pol=${sync_pol}:num-cpu=${num_cpu}:icount=${icount}:threading=${threading}:accel=${accel}) + foreach(mcips ${mcips_variants}) + foreach(num_cpu ${QBOX_CPU_TEST_NUM_CPU_COMBINATION}) + set(test_name ${target}:sync-pol=${sync_pol}:num-cpu=${num_cpu}:icount=${icount}:threading=${threading}:accel=${accel}:mcips=${mcips}) set (skip FALSE) foreach(tgt_name IN LISTS SKIP_TESTS) @@ -159,6 +168,9 @@ function(qbox_add_cpu_test target timeout_s) -p test-bench.inst_a.tcg_mode=${threading} -p test-bench.inst_b.tcg_mode=${threading} + -p test-bench.inst_a.enable_mcips_plugin=${mcips} + -p test-bench.inst_b.enable_mcips_plugin=${mcips} + -p test-bench.bulkmem.max_block_size=1024 -p test-bench.bulkmem.min_block_size=32 @@ -176,6 +188,7 @@ function(qbox_add_cpu_test target timeout_s) set_tests_properties(${test_name} PROPERTIES WILL_FAIL TRUE) endif() # -------------------------------------------------------------------------------- + endforeach() endforeach() endforeach() endforeach() diff --git a/tests/qbox/cpu/aarch64/dmi-test-concurrent-inval.cc b/tests/qbox/cpu/aarch64/dmi-test-concurrent-inval.cc index bab620fd..4711e537 100644 --- a/tests/qbox/cpu/aarch64/dmi-test-concurrent-inval.cc +++ b/tests/qbox/cpu/aarch64/dmi-test-concurrent-inval.cc @@ -94,9 +94,9 @@ class CpuArmCortexA53DmiConcurrentInvalTest : public CpuArmTestBench invalidated; std::mutex mutex; +protected: + gs::async_event* m_aevs; + public: CpuArmCortexA53DmiConcurrentInvalTest(const sc_core::sc_module_name& n) - : CpuArmTestBench(n), invalidated(p_num_cpu, false) + : CpuArmTestBench(n) + , invalidated(p_num_cpu, false) + , m_aevs(new gs::async_event[p_num_cpu]) { char buf[2048]; SCP_DEBUG(SCMOD) << "CpuArmCortexA53DmiConcurrentInvalTest constructor"; @@ -129,7 +134,7 @@ class CpuArmCortexA53DmiConcurrentInvalTest : public CpuArmTestBench(n) + CpuArmCortexA53DmiTest(const sc_core::sc_module_name& n) + : CpuArmTestBench(n), m_aevs(new gs::async_event[p_num_cpu]) { char buf[1024]; SCP_DEBUG(SCMOD) << "CpuArmCortexA53DmiTest constructor"; @@ -130,7 +131,7 @@ class CpuArmCortexA53DmiTest : public CpuArmTestBench jump end )"; +protected: + gs::async_event m_aev; + public: CpuHexagonLdStTest(const sc_core::sc_module_name& n) - : CpuTestBench(n), hex_gregs("hexagon_globalreg", &m_inst_a) + : CpuTestBench(n), hex_gregs("hexagon_globalreg", &m_inst_a), m_aev("aev") { for (int i = 0; i < m_cpus.size(); i++) { auto& cpu = m_cpus[i]; @@ -54,6 +57,7 @@ class CpuHexagonLdStTest : public CpuTestBench hex_gregs.p_hexagon_start_addr = MEM_ADDR; char buf[1024]; + m_aev.async_attach_suspending(); std::snprintf(buf, sizeof(buf), FIRMWARE, static_cast(CpuTesterMmio::MMIO_ADDR)); set_firmware(buf, MEM_ADDR); } @@ -77,6 +81,7 @@ class CpuHexagonLdStTest : public CpuTestBench { SCP_INFO(SCMOD) << "write, data: 0x" << std::hex << data << ", len: 0x" << len; passed = (addr == 0 && data == 0x0f0f0f0f && len == sizeof(int32_t)); + m_aev.async_detach_suspending(); } virtual uint64_t mmio_read(int id, uint64_t addr, size_t len) override { return 0; } diff --git a/tests/qbox/cpu/riscv32/riscv32-reset.cc b/tests/qbox/cpu/riscv32/riscv32-reset.cc index dc07d98f..e61c967c 100644 --- a/tests/qbox/cpu/riscv32/riscv32-reset.cc +++ b/tests/qbox/cpu/riscv32/riscv32-reset.cc @@ -161,11 +161,16 @@ class CpuRiscv32ResetGPIOTest : public CpuTestBench SCP_DEBUG(SCMOD) << "TB cache invalidation completed"; /* - * Triggers the reset, this is setup via "sensitive << reset_event" above - * Use a larger delay to ensure firmware loading and TB flushing complete first - * Increased delay for COROUTINE threading mode compatibility + * Triggers the reset, this is setup via "sensitive << reset_event" above. + * Use a delta notification (SC_ZERO_TIME) so it fires in the next + * delta cycle at the current simulation time. A timed delay (e.g. + * 1 SC_US) would require SystemC time to advance, which in mcips + * mode only happens when the sync_window moves forward, leaving + * the event stranded while the CPU spins in a tight MMIO loop. + * Firmware loading and TB invalidation are already complete before + * this point, so no additional delay is needed. */ - reset_event.notify(sc_core::sc_time(1, sc_core::SC_US)); + reset_event.notify(sc_core::SC_ZERO_TIME); break; case RESET_DONE: /*