Model development guide
This page explains the structure, lifecycle, and capabilities of a Model in the Sim Engine using the provided model2 sample as the reference. A Model is a logical unit that runs at a configured rate, may depend on other models, and contains state controllers that group related states (play/reset/pause).
Overview
Models encapsulate simulation behavior. Each model has a lifecycle and a collection of state controllers. Models can run in parallel with other models; dependencies between models should be expressed explicitly so the scheduler can respect ordering.
Primary responsibilities of a model:
- Register and initialize state controllers and resources
- React to engine lifecycle events (Reset, Init, Run, Stop, Freeze, FirstRun)
- Execute time-critical logic inside
OnRun() - Trigger async actions and pass descriptors to background threads (lockless)
- Publish/subscribe to data streams and recorders
Model Lifecycle Methods
The sample model defines standard overridable methods. Each is called by the engine at specific moments:
Model Constructor
Called once when the model is created. Use to:
-
Initialize one time objects. for example:
const int numOfTasks = 10; tasks = std::make_unique>CTasks<(numOfTasks); hAsyncThread = tasks->AddThread("AsyncThreadName1", [](void* pObj, void* param1, void* param2, void* param3) { ((CMyModel*)pObj)->AsyncCmd(nullptr, nullptr, nullptr); }, this); -
Configure the CPU affinity for the model. The example below sets it to use Core 0 and Core 2:
cores = { true, false, true }; -
Set the thread priority. The example below sets the thread to the highest priority level:
eThreadPriority = EThreadPriority::Highest;
Updating a Model's Version
Every model passes its name, version, and core affinity to the CModelBase base constructor:
C{{ModelName}}::C{{ModelName}}() : CModelBase("{{ModelName}}", "1.00", {{{CoreAffinityArray}}})
{
}
The version is a free-form string — there's no separate build step to keep it in sync. To "release" a new version of a model, just update that string literal. It's shown live per-model in DIPanel's Core States and System Layout's Status view, so mismatched versions across a deployment are easy to spot. Drivers work the same way — see the driver layer constructor.
OnInit()
Called once when the model is created or the engine initializes. Use to:
- Initialize state controllers (e.g.
CStatesController::AddController<T>("Group")) - Set initial state statuses (
SetStateStatus(...)) - Configure stream triggers and callbacks (
SetStreamTrigger,SetStreamBlockReceiveCB,SetStreamBlockPreSendCB,SetStreamBlockPostReceiveAcceptCB) - Call global initializers like
OGIInit()(only once from one model) - Register a custom keyboard handler, if this model needs to react to key presses (
KeyListener::SetCallbackHandler(...))
Custom Keyboard Handling
Register a handler once from OnInit(), then react to keys in the callback:
void C{{ModelName}}::OnInit()
{
KeyListener::SetCallbackHandler(HandleKeyEvent);
}
void HandleKeyEvent(const KeyEvent& k)
{
auto key = ::toupper(k.key);
if (key == 'X') {
// Do your logic for key 'X' pressed by the user
}
// Optional: fall through to the engine's default keyboard handler
// CSimEngine::HandleKeyEvent(k);
}
CSimEngine::SetupMainKeyboardHandler().
OnFirstRun()
Called the first time the model enters running state. Use for one-time start-up actions that must happen when the engine starts running (not just initialization).
OnRun()
Main per-frame execution callback. This is the hot path and must be fast and deterministic. Typical tasks:
- Execute each state controller (
CStatesController::ExecuteState("Group")) - Send or enqueue outputs to streams or drivers (
CDrivers::SendBlock) - Start async tasks without waiting for completion
- Collect lightweight profiling metrics or increment in-memory histograms
OnReset()
Called when the simulation is reset. Use to:
- Set the model's minor frame frequency via
SetMinorFrame()(e.g., -1 = every simulator minor frame) - Fetch shared resources (LRUs) and initialize pointers
- Reset internal counters and state
OnStop() and OnFreeze()
Called when the model is stopped or temporarily frozen. Use to safely quiesce resources, stop background operations, or mark state controllers as paused.
OnStateChanged()
Callback invoked when the Sim Engine state changes (e.g., Run <-> Step <-> Stop). Use to react to global engine state transitions.
States and State Controllers
Inside a model, states are grouped into logical State Controllers. Each controller manages a group of states and has its own play/pause/reset controls.
Why group states?
- Modularity: group related behaviors (communication, initialization, health checks)
- Control: pause or reset groups independently
- Sequencing: controllers allow deterministic ordering inside a model
Common operations
// Add controller and set status
CStatesController::AddController<CCommCheck>("CommChecks");
CStatesController::SetStateStatus("CommChecks", CACState::EStateStatus::ePlay);
// Execute controllers in OnRun
CStatesController::ExecuteState("CommChecks");
Rates & Dependencies
Each model executes at a configured minor frame rate. Rates can be:
-1— run every simulator minor frame0— disabled (does not run)- >0 — run every X simulator frames
Multiple models may run in parallel. If model A depends on model B, declare that dependency (via configuration or the scheduler) so the scheduler orders execution appropriately.
Example: a sensor model may run at 1 kHz while a heavy physics model runs at 125 Hz. The scheduler should ensure data produced by the physics model is propagated deterministically (e.g., via timestamped buffers) to the higher-rate consumer.
Streams, IO & Recording
Models can publish or subscribe to streams. Use stream triggers to run a model only when specific data arrives. Register callbacks around the raw send/receive path:
// Execute only when a block arrives on StreamName/BlockName
SetStreamTrigger("StreamName", "LRUName.BlockName");
// Called just before the raw block is sent out
SetStreamBlockPreSendCB("StreamName", "LRUName.BlockName", &Cmodel2::OnPreSendCB);
// Called just after the raw block is received, before it is accepted
SetStreamBlockPostReceiveAcceptCB("StreamName", "LRUName.BlockName", &Cmodel2::OnPostReceiveAcceptCB);
// Register receive callback
SetStreamBlockReceiveCB("StreamName", "LRUName.BlockName", &Cmodel2::OnReceiveCB);
All three callbacks share the same signature, which now includes the block name:
void Cmodel2::OnReceiveCB(const char* blockName, const uint8_t* raw, uint64_t size);
void Cmodel2::OnPreSendCB(const char* blockName, const uint8_t* raw, uint64_t size);
bool Cmodel2::OnPostReceiveAcceptCB(const char* blockName, const uint8_t* raw, uint64_t size);
- SetStreamBlockPreSendCB — called just before the raw data is sent out. Useful for calculating a CRC or otherwise finalizing the raw buffer before transmission.
- SetStreamBlockPostReceiveAcceptCB — called just after the raw data is received, before it is accepted. Useful for validating the raw message. If it returns
false, the message counter is not incremented and the registered stream trigger is not called. - SetStreamBlockReceiveCB — called after a received block has been accepted.
For recording, prefer pushing small descriptors into a lockless SPSC ring in the hot path and let the recording thread serialize or write the large data off the critical path.
Raw ↔ Eng Conversion
Every ICD block generated by DBSimGenerator has a raw
form (the exact bytes on the wire) and an eng (engineering) form — the ordinary C++
value your model code reads and writes. The conversion is called once per LRU — it
converts every block belonging to that LRU in one call, not one call per block. Convert explicitly in
OnRun():
void C{{ModelName}}::OnRun()
{
// Convert Raw to Eng (after new raw data has arrived on a stream) — converts all blocks of this LRU
CLRUsProj::{{LRUName}}->Raw2Eng();
// ... read/modify the eng-side elements of any of this LRU's blocks here ...
// If you modified eng values and need them sent out, convert back before returning:
CLRUsProj::{{LRUName}}->Eng2Raw();
}
Which conversion a given block uses is a property of that block, set once in DBSimGenerator, not something you choose per call:
| Convention | Meaning |
|---|---|
NoRaw | The block has no physical/binary raw representation (e.g. purely internal/software-only data) — there is nothing to convert. |
LElsb | Little-Endian byte order, bits numbered from the Least-Significant Bit (bit 0 = LSB) — the natural C/C++ convention. |
LEmsb | Little-Endian byte order, bits numbered from the Most-Significant Bit. |
BElsb | Big-Endian byte order (network/wire order), bits numbered from the LSB. |
BEmsb | Big-Endian byte order, bits numbered from the MSB — common in avionics/MIL-STD style ICDs. |
Raw2Eng()/Eng2Raw() code for each block's chosen convention. You only need to know which convention a block uses when cross-checking its layout against an external ICD document or a driver's wire format.
Skip Unchanged Data
Use CSimEngine::CValueChangeGate to skip processing an element that hasn't
changed since the last call — useful right after a Raw2Eng() conversion, before doing
any expensive work with the converted value.
Async Commands
Async tasks
For long-running or blocking operations (file I/O, network, complex computation), use a thread pool or async task manager. Important rules:
- Do not block the real-time
OnRun()while waiting for async work. - Check task availability before starting to avoid duplicate runs (
tasks->IsTaskBusy()). - Prefer to enqueue and forget; use status flags or callbacks to detect completion.
Syncing an async thread to the frame cycle
When an async thread needs to run in lockstep with CoreEngine frames (rather than fire-and-forget), register it
with CSimEngine::PhaseCoordinator and wait on a barrier for its assigned phase:
void C{{ModelName}}::AsyncCmd(void* p1, void* p2, void* p3)
{
auto& barrier = CSimEngine::PhaseCoordinator.GetBarrier({{Phase}});
barrier.Register();
uint64_t generation = 0;
while (true)
{
barrier.WaitForPhase(generation);
// ... do work for this phase ...
barrier.NotifyDone();
}
barrier.Unregister();
}
Guarding against multiple calls per minor frame
A model can have several stream triggers or callbacks that all funnel into the same shared logic. If more than one of them can fire within the same minor frame, guard the shared code with CSimEngine::CMinorFrameGate so it only executes once per frame no matter how many times it's called:
static CSimEngine::CMinorFrameGate frameGate;
if (!frameGate.ShouldRun())
return;
// this code runs at most once per CoreEngine minor frame
Profiling
Keep profiling lightweight in the hot path: use scope profilers or running averages. Avoid heavy logging. Examples from the sample:
// scope profiler
{
static CRunningAverage runningAverage;
CSimScopeProfiler simScopeProfiler("profilingTaskName", &runningAverage);
}
// manual timing
auto startT = std::chrono::high_resolution_clock::now();
// ... work ...
runningAverage.add(std::chrono::duration_cast(
std::chrono::high_resolution_clock::now() - startT).count());
Typical Model Flow (Example)
OnInit():
- Initialize states and controllers
- Set stream triggers and callbacks
- Pre-allocate buffers
OnReset():
- Set minor frame using SetMinorFrame()
- Reset state controllers and counters
OnFirstRun():
- One-time start tasks (timers, warm-up)
OnRun():
- Execute state controllers (fast, deterministic)
- Enqueue record descriptors into ring buffer
- Optionally start async tasks (non-blocking)
OnStop()/OnFreeze():
- Quiesce background threads, flush buffers
Integration & Best Practices
- Pre-allocate all model-local buffers and structures in
OnInit(). - Avoid blocking operations in
OnRun(). Use lockless handoffs to background threads. - Set
SetMinorFrame()appropriately inOnReset()depending on how often the model should run. - Use state controllers to group related functionality and to enable/disable features at runtime.
- Prefer descriptive, small messages to the recorder (pointer or index) rather than raw blobs in the hot path.
- Document cross-model dependencies and ensure the scheduler respects ordering.