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¶
- Entity object model — the S0–S11 refactor steps behind this decomposition.
- Execution model — quanta, pacing, idle-skip, the three execution paths.
- Plugin system — the host-facing plugins the
Socstarts and stops. - Design decisions — Decisions 65 and 66.