# Architecture This page is for contributors and for anyone embedding cpptb in their own build or regression system. It describes how the runtime is layered, where the simulator boundary sits, and which ownership rules the design depends on. Nothing here is needed to write a testbench. The repository contains a reusable framework and an optional reference harness. The framework has four separable layers: 1. `coro_runtime.hpp` owns tasks, processes, waits, timers, events, queues, and scheduler ordering. 2. `test_api.hpp` owns the compiled test catalog, one-test selection, assertions, process lifetime, and exception attribution. 3. `dpi_runtime.hpp` translates simulator callbacks into scheduler steps and batches changed values across the DPI boundary. 4. `cpptb-codegen` elaborates RTL and emits a typed DUT plus transport wrapper — the pipeline [How a build works](how-it-works.md) walks. The reference harness has two layers: 1. The public `cpptb` project layer resolves source conventions or `cpptb.toml`, owns the two-pass build, and caches simulator artifacts. 2. The runner discovers tests and starts one fresh simulator process per selection; `cpptb-run` exposes that lower-level executable protocol alone. `test_result.hpp` and `test_reporting.hpp` sit beside these layers as an embeddable result contract. A higher-level harness can call `registered_tests()` and `run_registered_test(...)`, receive `ResultSink` callbacks, or consume versioned JSON without using the command-line launcher. The lifecycle contract and its current limitations are documented in [Framework test lifecycle](test-lifecycle.md). The generated SystemVerilog wrapper owns clocks and simulator callback timing ([Clocking](clocking.md) covers the user-facing contract). The C++ scheduler owns coroutine readiness and cancellation. Signal reads and writes use generated IDs and typed bindings; no runtime hierarchical-name lookup is required on the optimized DPI path. Every registered test invocation creates one shared lifecycle state. Processes started through its `TestContext` are retained as test-owned work. Normal root completion, a fatal requirement, or an uncaught child exception cancels any remaining owned processes before the result becomes terminal. Generic low-level scheduler roots still abort on an unobserved exception, so exceptions cannot disappear silently outside the test lifecycle boundary. `Scheduler`, `Testbench`, `TestContext`, and their `Process` handles are confined to the simulator thread that owns the testbench. They are not thread-safe and must not be copied to worker OS threads. This matches the single-threaded scheduler and simulator-callback contract and lets lifecycle ownership use non-atomic reference counting on the process hot path. Parallel host work must return its result through a simulator-thread integration point rather than calling framework APIs directly. Coroutine frames are cached in one process-wide pool under that ownership contract. This avoids a thread-local lookup for every task creation and reclamation. An embedding that runs independent simulator runtimes on separate OS threads may define `CPPTB_CORO_THREAD_LOCAL_FRAME_POOL` to give each thread its own pool; framework objects and callbacks must still remain on their owning simulator thread. Simulator-phase waits have a backend boundary below the public API; the user-facing selection and status table is [Timing backend support](scheduling.md#timing-backend-support). The portable path registers standard VPI callbacks for read/write synchronization, read-only synchronization, and the next timestep. When cpptb owns Verilator's host loop, its direct backend polls the same pending phase state at those exact host-loop locations and dispatches the generated DPI phase task without allocating one-shot VPI callback records. The VPI path remains available for other simulators and Verilator builds that require external VPI integration. Both supported modes continue to use generated DPI for signal transport; only their simulator-phase dispatch mechanism differs. Generated pure-DPI timing paths are retained as experiments, not supported backends. An NBA-barrier phase pump reproduces the timing contract but is slower than portable VPI. A centralized generated calendar instead gives one SystemVerilog process ownership of compile-time-discovered clocks, framework timers, and phase dispatch; it passes conformance and the exact performance guard under Verilator. External and DUT-driven clocks remain thin observers. The calendar only knows generated clocks, framework timers, and explicitly observed signals. Standard DPI has no simulator-wide next-event query, so arbitrary internal delayed DUT activity still requires generated observation or a simulator callback API. Cross-simulator validation is required before the calendar can become a supported portable backend. The resulting backend policy is: 1. Use direct timing dispatch when cpptb owns the Verilator host loop. 2. Use standard VPI callbacks as the supported portable fallback. 3. Keep the generated SV-DPI calendar feature-gated until its restricted `NextTimeStep` semantics and cross-simulator behavior are acceptable. The framework keeps clock-cycle waits and absolute delays separate. Any signal configured as an observable clock can drive edge waits, while the persistent timer owner supports clockless and arbitrary multi-clock designs. ## Reusable DPI runtime `include/cpptb/dpi_runtime.hpp` owns the design-independent DPI host behavior: - compact directional input/output transport and driven-signal tracking; - typed signal `get()`/`set()` callbacks and dirty-output detection; - generated internal-probe `get()`/`deposit()` access for packed variables and fixed memories; - scheduler construction, edge dispatch, and delay deadlines; - falling-edge interest and precision-aware time transport; - timeout invocation, elapsed wall time, completion, and result reporting; - the standard init, step, output-pull, deadline, and edge-interest C exports expected by the generated wrapper. `include/cpptb/test_result.hpp` keeps status, check counts, timing, and structured failure records independent of DPI. `test_reporting.hpp` writes the versioned JSON result consumed by the optional launcher, so user-facing fixtures and embedding harnesses do not include simulator transport headers. A design supplies a small `DpiAdapter` containing its DUT and result types, generated signal metadata, binding call, testbench registration call, result name, and timeout policy. `CPPTB_DEFINE_DPI_RUNTIME(Adapter)` provides the C entry points. No design transport needs to copy open arrays, decode events, or format a result line. The hot scheduler step receives only the compact observed-word array. Driven words are fetched through a separate idempotent output-pull export on initialization or after `STEP_OUTPUTS_CHANGED`, so unchanged steps do not carry an output argument through the simulator ABI. `deposit()` performs the underlying SystemVerilog blocking assignment immediately. A port `set()`, by contrast, queues in the scheduler's deferred-write buffer and flushes at the timestep's ReadWrite dispatch — the scheduler owns that queue and its flush point, and the transport only ever carries flushed words. Neither operation inserts a scheduler delay or observation phase; testbench code uses an explicit `co_await ReadOnly{}` or `Delay{...}` when downstream RTL must evaluate before observation. See [The write model](scheduling.md#the-write-model). ## Current scope The end-to-end test suite currently targets Verilator. Scalar signal values use `uint32_t`; packed values use `uint64_t` through 64 bits and `Bits` above 64 bits. Generated DPI bindings support fixed multidimensional unpacked arrays, packed enum and struct views, wide values, fixed-point helpers, and generated hierarchical probes with read, deposit, force, and release operations. Four-state X/Z propagation remains deferred. The generated transport uses standard SystemVerilog DPI, but additional simulator backends have not yet passed the conformance suite. The Authoring Core sources currently present under `benchmarks/authoring_core/` exercise typed tasks, cycle waits, edge timeouts, predicate waits, events, bounded queues, locks, semaphores, wide packed signals, fixed-point arithmetic, fixed unpacked arrays, and a synchronous memory front door. Its C++ DPI testbench is `benchmarks/authoring_core/testbenches/cpp_dpi/testbench.cpp`, the corresponding pure-SV source is `benchmarks/authoring_core/testbenches/systemverilog/authoring_core_sv_tb.sv`, and the shared workload contract is `benchmarks/authoring_core/workload.py`. Runtime API tests are in `tests/unit/coro_runtime_test.cpp`.