Shared SimLink C Architecture

August 11, 2026 ยท View on GitHub

This directory contains the simulator-neutral Stream, Memory, and SideBand models, instance registry, and ZeroMQ transport. See the architecture reference for the layered design and the protocol reference for wire contracts.

Components

LayerFilesResponsibility
Stream model/codecRogueTcpStreamModel.h, RogueTcpStreamCore.h, RogueTcpStreamCore.cAXI Stream signal state, frame codec, and clock-step FSM
Memory model/codecRogueTcpMemoryModel.h, RogueTcpMemoryCore.h, RogueTcpMemoryCore.cAXI-Lite signal state, transaction codec, and clock-step FSM
SideBand model/codecRogueSideBandModel.h, RogueSideBandCore.h, RogueSideBandCore.cOpcode/remote-data state, message codec, and clock-step FSM
TransportRogueSimLinkTransport.[ch]Worker-owned sockets, bounded message handoff, timeouts, and shutdown
Instance registryRogueSimLinkInstance.[ch]Model allocation, handles, validation, cleanup, and process-wide port-pair ownership
Backend adapter../ghdl, ../vcs, or ../xsimSimulator ABI translation, callbacks, and diagnostics
Stream pacing../sim/RogueTcpStreamPacer.vhdDeterministic simulated-time payload serialization

The compiled cores depend on adapter-provided Rogue*Log() and Rogue*Fatal() hooks, but contain no simulator API code. The adapters contain no protocol state or wire framing.

backend callback/update
  -> copy simulator inputs to inSnap[]
  -> Rogue*Step(data)
       -> poll the inbound worker FIFO
       -> advance the codec/protocol FSM
       -> rendezvous with the worker for complete outbound messages
       -> update outState[]
  -> publish outState[] to the simulator

Ownership and lifecycle

ObjectOwner/contextLifetime
Model state and snapshotsSimulator instanceCreate/elaboration through destroy/process exit
ZeroMQ context and socketsOne transport worker per modelFirst post-reset step through worker shutdown
Handle and complete port pairRogueSimLinkInstance registryExplicit destroy or process atexit
GHDL integer handleProcess-wide GHDL registryProcess lifetime or atexit
xsim chandleSystemVerilog DPI leafFirst rising edge through final/atexit
VCS callback metadataVHPI adapterElaboration through process exit

RogueSimLinkInstance allocates zeroed model state, validates model ownership, and registers fallback cleanup. A model's static descriptor address is its type token, while the descriptor name is used only for diagnostics. This lets a new model define its own identity without changing a central model list. Registry membership is established before an opaque pointer is dereferenced, so null, fabricated, stale, and wrong-model contexts fail safely without a magic value in model storage. The model clears protocol-visible state while reset is asserted. On the first post-reset rising edge, the registry reserves the complete portNum/portNum+1 pair before the worker binds either socket. The reservation remains immutable until cleanup.

CREATE -> RESET -> RESERVE PORTS -> START WORKER -> BIND -> RUN
                                                         |
       release pair <- release model <- join <- DESTROY -+

During destruction, the instance is first removed from the live registry. The worker then stops accepting work, wakes from finite polling, closes both sockets with zero linger, and joins before the model and port pair are released. GHDL and xsim have explicit/final cleanup paths; VCS destroys the common instance through process-exit cleanup. Its full VHPI metadata cleanup routine remains explicit because registering a VCS end callback is unsafe in the cocotb flow.

Model state

Stream

Stream owns fixed inbound/outbound frame buffers, sizes and cursors, SSI user fields, and output-valid state. It gathers kept bytes until TLAST, sends one four-part message, and emits received payload while honoring obReady. Buffers are 20,000,000 bytes per direction and instance. Partial final beats read only valid payload bytes; invalid lanes are zero and masked by TKEEP.

Memory

Memory owns one request/completion, a fixed data buffer, transaction metadata, the current 32-bit word offset, AXI-Lite FSM state, and the retained result. The readiness probe is handled locally; other operations issue sequential 32-bit AXI-Lite accesses. A multiword transaction retains its first non-OKAY RRESP or BRESP. Post keeps the historical completion frame, which Rogue discards because it completes Post locally.

SideBand

SideBand retains transmitted/received remote data and opcode/value-valid state. A received opcode is a one-clock event; received remote data persists. Send flags are cleared after each event so later updates cannot inherit stale qualifiers.

Transport policy

Each model has one worker that exclusively owns its ZeroMQ context and PULL/PUSH sockets. Only complete multipart messages cross the thread boundary:

  • inbound messages enter a 16-message FIFO; when full, the worker stops draining ZeroMQ and does not drop or overwrite entries;
  • cumulative message size is checked before allocation (MAX_FRAME plus metadata for Stream, MAX_DATA plus metadata for Memory, and four bytes for SideBand);
  • outbound messages use a single-message rendezvous and PUSH high-water mark of one, so a stalled peer cannot be hidden by a large ZeroMQ queue;
  • SURF_SIMLINK_TRANSPORT_TIMEOUT_MS overrides the 30-second wall-clock timeout with a positive decimal millisecond value;
  • socket creation, options, bind, polling, sends, and joins all have checked or bounded failure paths.

Socket policy uses ZMQ_IMMEDIATE=1, ZMQ_LINGER=0, PUSH/PULL high-water marks of 1/16, a 100 ms worker send timeout for observing stop requests, and 10 ms polling. Worker errors are copied as data; only the simulator thread calls backend logging or fatal hooks.

Concurrency invariants

  • One worker owns and uses a socket for its entire lifetime.
  • Only complete protocol messages cross a thread boundary.
  • Only the simulator context mutates model state.
  • A port pair is released only after both sockets close.
  • Reset has an explicit queue policy and cannot race socket destruction.
  • Host queue depth and scheduling do not define simulated link bandwidth.
  • Every wait and join has a bounded failure path.

Timing boundary

Transport handoff uses host wall-clock time. Stream bandwidth is modeled in simulation time by RogueTcpStreamPacer, which counts valid TKEEP bytes on AXI handshakes and caps saved credit at one beat. The shared Stream codec stores beats as little-endian 32-bit-word arrays and supports 1 through 128 data bytes; each backend translates its native vector ABI to that representation. See the bandwidth contract for equations. Memory and SideBand do not provide rate shaping.