Skip to content

Runtime decomposition

Emulator is a facade. The 2026-06 entity-model refactor (steps S4/S5/S6) extracted its state and behaviour into three subsystems held as plain members: Soc (the assembled machine), DebugServer (GDB stub + breakpoints), and ExecutionEngine (cores, run loop, clock, JIT). What remains in the facade is config validation, image loading, and one-line delegation: src/runtime/include/tero/runtime/emulator.hpp is 275 lines and src/runtime/src/emulator.cpp is 357 lines, of which the run_* / state / memory entry points forward to a member (emulator.cpp:258-325).

The split is recorded as Decision 65 (decisions.md); the canonicality of the per-core register blob, with core(idx) kept as the SPARC lens over it, is Decision 66 (decisions.md).

Responsibilities

Class Owns Borrows Does not own
Soc (soc.hpp:71) The entity graph: bus::SystemBus bus_, the AMBA topology overlay (apb_ / ahb_), the comms-bus entities (buses_), peripherals_, irq_bridges_, the per-UART chardevs (uart_chardevs_), the host-facing plugins_ (soc.hpp:174-197); the by-name IEntityRegistry over all of them config_ / logger_ / observer_ / scheduler_ / time_ — injected at build(), stored as raw pointers (soc.hpp:165-169) The cores, the simulated clock, the GDB stub
ExecutionEngine (execution_engine.hpp:66) Per-core canonical register blobs core_blobs_ (ir::GuestState) and run latches core_ctrl_ (CoreControl) (execution_engine.hpp:257-263); the CPU entities cpu_entities_ (:279); one CpuBusBridge per core (:283); sim_time_ + EventScheduler scheduler_ (:247,323); the guest architecture ir_arch_ and the per-core IR caches / interpreters / TieredJit (:307-321); the MultiThread workers and barriers (:288-296) config_ / logger_ / observer_ / soc_ / debug_ — injected at initialize() (execution_engine.hpp:241-245); a cached interrupt_controller_ view read from the SoC (:327) The bus and peripherals it drives, the breakpoint set, the GDB stub
DebugServer (debug_server.hpp:31) BreakpointSet breakpoints_ + std::unique_ptr<GdbStub> (debug_server.hpp:66-67) The Emulator the stub binds to, passed at start() (debug_server.hpp:46) — the stub still reads guest state and memory through the whole facade (debug_server.hpp:27-30) Cores, bus, scheduler
Emulator (emulator.hpp:29) The three subsystems plus the cross-cutting services logger_, fault_injector_, observer_ (emulator.hpp:242-244) Nothing the subsystems own twice: there is no duplicated core / bus / stub state on the facade

Emulator implements IEntityRegistry by delegating find_entity to soc_, which owns the graph (emulator.cpp:188-192).

Initialization order

Emulator::initialize() (emulator.cpp:138-179) runs three steps:

Emulator::initialize()
  1. soc_.build(config_, *logger_, observer_.get(),
                engine_.scheduler(), engine_.time_source())   // emulator.cpp:149
  2. engine_.initialize(config_, *logger_, observer_.get(),
                        soc_, debug_)                          // emulator.cpp:160
  3. debug_.start(*this, config_.gdb_stub_port,
                  config_.gdb_stub_wait_for_client, *logger_)  // emulator.cpp:172

The ordering breaks the Soc↔engine dependency cycle. Soc::build injects a scheduler and a time source into every PeripheralContext (soc.cpp:283-289); ExecutionEngine::initialize needs the assembled SoC (bus, peripherals, interrupt controller) to build the cores, bridges, and JIT over it. The cycle resolves because the engine's EventScheduler and SimTimeSourceAdapter are members, live from construction — scheduler() and time_source() are valid before engine_.initialize runs (execution_engine.hpp:167-169), so the SoC borrows them first.

debug_.start runs last: GdbStub binds to the whole Emulator (debug_server.cpp:18), and wait_for_client may block until a client attaches. With gdb_stub_port == 0 the call is a no-op (debug_server.cpp:15-17).

Soc::build assembly sequence

Soc::build (soc.cpp:111-268) assembles the machine in this order:

Step What Where
1 Map RAM; map a 4 MiB low-RAM mirror at 0x0 when ram_base != 0 and no PROM covers it; map the AMBA PnP scratch regions soc.cpp:120-142
2 Read the PROM image file into the config blob; flatten an mkprom2 ELF wrapper into a flat PROM buffer soc.cpp:145-171
3 create_peripherals_from_specs(): run each spec's factory in vector order, allocate one IrqBridge per declared IRQ, inject the chardev, call set_instance_name (soc.cpp:367), discover the interrupt controller and GPTimer, attach(); the PROM peripheral is wired after the specs soc.cpp:173,310-409
4 connect_peripheral_ports(): construct the declared comms-bus media, then the unified slot ← interface connection pass; runs before MMIO mapping so wiring has no MMIO side effects soc.cpp:179
5 create_plugins_from_specs(): instantiate, attach, and start the host-facing plugins on the now-connected buses soc.cpp:184
6 map_mmio for every peripheral soc.cpp:188-193
7 Engage the bus / interrupt-controller GatedMutex gates when execution_mode == MultiThread — before any core thread exists soc.cpp:199-203
8 Program the GPTimer boot scaler so timer decrements run at 1 MHz at any cpu_clock_hz soc.cpp:208-221
9 Build the AMBA topology overlay (apb_ / ahb_) and populate the PnP entries from the live entities soc.cpp:229-265

ExecutionEngine::initialize (execution_engine.cpp:70) then constructs the guest architecture via the factory (or cfg.arch_factory), reserves core_blobs_ / core_ctrl_ to num_cores (never resized — the architecture's lens pointers stay valid), builds the per-core CpuBusBridges and CPU entities, constructs the IR caches / interpreters / JIT tiers when translation is set, starts the MultiThread workers, resets every core, and applies the configured reset PC with secondary cores parked.

Teardown contract

Destruction order is encoded in member declaration order in emulator.hpp (emulator.hpp:239-264, restated at emulator.cpp:51-56). C++ destroys members in reverse declaration order, so:

Order Member What its destructor needs
1st engine_ (declared last, emulator.hpp:264) ~ExecutionEngine joins the worker threads (execution_engine.cpp:35-39); workers touch the SoC bus through the per-core CpuBusBridges, so soc_ must still be alive
2nd debug_ (emulator.hpp:256) Inside DebugServer, breakpoints_ is declared before gdb_stub_, so the stub destructs first while the set it references is alive (debug_server.hpp:63-67)
3rd soc_ (emulator.hpp:251) ~Soc stops the host-facing plugins up front — they hold sockets and reference the buses (soc.cpp:87-96); inside Soc, plugins_ is declared last so it destructs before the buses it observes (soc.hpp:171-197)
4th observer_, fault_injector_, logger_ (emulator.hpp:242-244) Borrowed by both soc_ and engine_ throughout, so they destruct after both

This is a falsifiable invariant: reordering the members of Emulator in emulator.hpp (or of Soc / DebugServer in their headers) is a teardown bug, even though it compiles.

flowchart TD
    EMU["Emulator<br/>(facade, emulator.hpp:29)"]
    SOC["Soc<br/>(entity graph)"]
    DBG["DebugServer<br/>(BreakpointSet + GdbStub)"]
    ENG["ExecutionEngine<br/>(cores, run loop, clock, JIT)"]

    EMU -- "owns soc_ (emulator.hpp:251)" --> SOC
    EMU -- "owns debug_ (emulator.hpp:256)" --> DBG
    EMU -- "owns engine_ (emulator.hpp:264, destructs first)" --> ENG
    ENG -- "borrows Soc* soc_ (execution_engine.hpp:244)" --> SOC
    ENG -- "borrows DebugServer* debug_ (execution_engine.hpp:245)" --> DBG
    SOC -- "borrows engine_.scheduler() / time_source()<br/>(soc.hpp:168-169, emulator.cpp:149)" --> ENG
    DBG -- "GdbStub binds Emulator& at start()<br/>(debug_server.cpp:18)" --> EMU

ExecutionEngine translation units

The engine implementation is split across six translation units in src/runtime/src/; each non-construction TU carries a \file header naming its contents.

TU Contents
execution_engine.cpp Construction / destruction, initialize() (architecture factory, blobs / latches / CPU entities / bridges, JIT tiers, workers, core reset), reset(), apply_elf_entry / apply_binary_entry, the core(idx) SPARC-lens accessor, opcode_histogram()
engine_run_loop.cpp Scheduling: pacing slices, the round-robin round (idle-skip, GDB attach points, time-advance fold), and the per-core quantum dispatch across the three execution paths (JIT / IR interpret / switch) (engine_run_loop.cpp:3-6)
engine_translate.cpp Translation paths: the tiered-JIT quantum (run_ir_quantum), the universal IR-interpret quantum (entity-model S9), JIT region discovery, and the code-cache flush plumbing (engine_translate.cpp:3-6)
engine_irq_time.cpp Interrupt sampling and delivery through the arch seam, and the global %asr22:%asr23 up-counter re-base (engine_irq_time.cpp:3-5)
engine_mt.cpp MultiThread orchestration (ADR-001): per-core worker threads, the start/done barriers, and the quantum-batch a worker runs between barriers (engine_mt.cpp:3-6)
engine_oracle.cpp The IR-vs-core::step oracle-lockstep harness (SPARC-by-design): every clean-exit IR block is validated against a scratch switch run over an overlay bus before it commits (engine_oracle.cpp:3-6)

CPU entities

src/runtime/include/tero/runtime/cpu.hpp defines the per-core entities the engine builds once in initialize() and stores in cpu_entities_ (execution_engine.hpp:279). They are non-owning facades: the engine owns the register blobs and drives execution; the entities reference that state.

Type Exposes Backed by
SparcCpu (cpu.hpp:45) IEntity, ICpu, core::ISparc, IGdbRegisters (72-word g-packet) A core::CpuState& lens + the shared ir::IArchitecture&; references, does not own (cpu.hpp:50)
Leon3 / Leon4 (cpu.hpp:104,110) Same as SparcCpu Thin model wrappers; the SPARC behaviour is identical, only the model identity differs — make_cpu(family, …) maps GR712RC → Leon3, GR740 → Leon4 (cpu.hpp:118)
GenericCpu (cpu.hpp:128) IEntity, ICpu The engine-owned ir::GuestState& blob + CoreControl& latches + ir::IArchitecture&; register and PC access go through the architecture, so the entity names no ISA

Emulator::cpu(idx) returns the entity as IEntity&; consumers navigate to the capabilities with get_interface<ICpu>() or get_interface<core::ISparc>() (emulator.hpp:209-214). The SPARC-typed core(idx) accessor remains the lens over the same blob bytes — Decision 66 (decisions.md).

See also