Skip to content

Hardware Aggregator Internals

The Slot and Table pages show what the Hardware Aggregator does; this page shows how it is put together, under src/model/hwCollection/dataStructure/. None of it introduces new primitive hardware: an aggregator is composition — plain Reg and Wire components plus update events, arranged by metadata.

Layout description vs instantiated hardware

Section titled “Layout description vs instantiated hardware”

The split runs through src/model/hwCollection/dataStructure/slot/:

  • FieldMeta (slotMeta.h) is one field’s description: _name and _size, nothing else.
  • SlotMeta is a std::vector<FieldMeta> with the layout algebra the user pages describe (operator+, operator-, addField, index/name/range slicing) and matchByName, which computes the matched source/destination index pairs that best-effort slot copy runs on. It holds no hardware.
  • Slot (slot.h) is the base class that pairs a SlotMeta _meta with a std::vector<HwFieldMeta> _hwFieldMetas. HwFieldMeta is the instantiated side of one field — the read face and the write face of whatever component backs it:
struct HwFieldMeta{
Operable* _opr = nullptr;
Assignable* _asb = nullptr;
};

Slot itself is backing-agnostic: its genAssignMeta* / genGrpAsmNode helpers build AssignMetas and AsmNodes purely against those two faces, and the virtual doGlobAsm overloads are what subclasses override to decide where a finished AsmNode goes.

RegSlot and WireSlot: minting the primitives

Section titled “RegSlot and WireSlot: minting the primitives”

RegSlot::initHwStructure (regSlot.cpp) creates one Reg per field with the mOprReg maker — the runtime-named sibling of mReg, so each field still passes through _make<> and registers with the ModelController like any user component. The register’s name is stamped from the layout (prefix + "colIdx_" + idx + "_" + fieldName), and the same pointer is stored as both _opr and _asb. WireSlot::initHwStructure (wireSlot.cpp) does the same with mOprWire and prefix + "_" + fieldName.

Their doGlobAsm(AsmNode*) overrides reveal why all slot updates are CCOs: RegSlot hands the node to ctrl->on_reg_update(asmNode, nullptr) and WireSlot to ctrl->on_wire_update(...) — the exact controller entry points a bare reg <<= uses, so the node is recorded into the current flow block and each field write becomes an update event in that field’s pool. (WireSlot rejects <<= with an mfAssert — wires have no edge to assign on.)

There is a second, controller-free route: AsmNode::dryAssign() — commented “assign with no flow block related” in asmNode.h — pushes each AssignMeta straight into the destination pool as an unconditioned event. The aggregator plumbing uses it for structural wiring that must exist regardless of flow position: the mux builders, dynamic-read views, the WireSlot(const Slot&) copy constructor, and WireSlot::addWire(name, opr), which grafts an extra driven field onto an existing view.

slot[idx] returns a SlotDynSliceAgent subclass (RegSlotDynSliceAgent / WireSlotDynSliceAgent) holding the master slot, the index operable, and an _isOH flag. OH (dataStructure/indexing/index.h) is just a marker struct wrapping an Operable&; passing it flips the per-field match condition from a binary compare (requiredIdx == i) to a one-bit slice of the index (requiredIdx.sl(i)), turning the decoder into direct one-hot enables.

The write path builds one AssignMeta per field plus that per-field precondition (Slot::genGrpAsmNode), then goes through the controller route above. The read path v() builds a fresh wire named slotSlice at the slot’s getMaxBitWidth() and adds one update event per field to its pool via createUEHelper: the first eligible field is the unconditioned default at DEFAULT_UE_PRI_MIN, later fields are condition-guarded at DEFAULT_UE_PRI_USER.

Table: rows, agents, and the reduction tree

Section titled “Table: rows, agents, and the reduction tree”

Table (table.h) is a SlotMeta plus std::vector<RegSlot*> _rows, built by buildRows as one RegSlot per row named prefix_i. An _isMasterTable flag records ownership: row/column slices and operator= produce views that share the same RegSlot* pointers with the flag cleared, so only the originating table deletes rows.

flowchart TB
    T["Table<br/>SlotMeta and N rows"] -->|"binary or OH row index"| TSA["TableSliceAgent"]
    TSA -->|"static or dynamic column"| TSAD["TableSliceAgentDouble"]
    TSA -->|"v() materializes a row view"| WS["WireSlot read view"]
    T -->|"_rows owns"| RS["RegSlot per row"]
    RS --> HFM["HwFieldMeta per field<br/>_opr read face and _asb write face"]
    WS --> HFM
    HFM --> PRIM["Reg or Wire primitive<br/>minted by mOprReg or mOprWire"]
    PRIM --> POOL["field UpdatePool<br/>events resolve by priority"]

table[idx] returns a TableSliceAgent (row selected); its v() calls genDynWireSlotBase, which emits per-row AssignMetas guarded by createIdxMatchCond (binary compare or one-hot bit slice) and dryAssigns them into a fresh WireSlot. Writes through the agent instead route Table::doGlobAsm’s pooled AsmNode to ctrl->on_reg_update — again a CCO. Indexing the agent once more yields a TableSliceAgentDouble — “double” meaning both dimensions are selected: operator()(int / name) fixes the column statically, operator[](Operable&) selects it dynamically, and v() correspondingly picks a field from the row view or dynamic-slices it.

The search machinery is a tournament fold. ReducNode pairs a WireSlot* with an optional index Operable*; doReduceBase pops nodes pairwise from a queue, asks the user comparator for a single-bit selectLeft, and createMux merges the pair field-by-field with AssignMeta::mux — muxing the carried indices the same way — until one node remains. doReducBinIdx / doReducOHIdx seed the queue with Val constants (i or 1 << i) so the winning row’s index falls out of the tree. Ordered search (findMBO_BIDX / findMBO_OHIDX) first calls augmentForOrderedSearch to graft two wire fields onto each row view — userValidCompare (the user predicate) and systemInOldestSec (oldestStartIndex <= rowIdx) — then reduces with a fixed newest/oldest comparator and slices the augmentation back off the result. The standalone mux builders (dataStructure/mux/mux.h) are the same shape in miniature: conditioned AssignMetas on a fresh wire, dryAssigned, folded one sel bit per tree level.

MemTable (dataStructure/memTable/) swaps the row dimension into memory: instead of N RegSlot rows it keeps one MemBlock per column (std::vector<MemBlock*> _memStorages), with depth inside each block. genDynWireSlot wires a WireSlot view from (*_memStorages[col])[*index] per column, and doGlobAsm writes name-matched fields through each MemBlockEleHolder — both via dryAssign. There is no reduction or search machinery: rows are no longer individually visible in parallel.

The simulator observes aggregates through SlotSimProbe (src/sim/modelSimEngine/hwCollection/dataStructure/slot/slotProber.h) and TableSimProbe (.../table/tableProber.h). A probe reads each field’s current 64-bit value through HwFieldMeta::_opr, diffs it against a prevValues snapshot, and reports FieldSimInfo64 records — TableSimProbe::detectRowChange promotes any changed field to its whole row. The Kride case study’s sim recorders (src/example/o3/simulation/) are their consumers; how simulation itself executes update events is the sim engine story.