Skip to content

Simulation Engine Internals

Rodrigo Torres edited this page Apr 23, 2026 · 1 revision

This page covers the code-level implementation of wiRedPanda's simulation engine: the four-state logic system, simulation loop phases, topological sorting, wiring data structures, and feedback loop handling.

For a conceptual overview of the simulation model, see the Simulation Guide.


Four-State Logic

All signals in wiRedPanda use four states, defined in App/Core/Enums.h:

State Value Meaning Visual Color
Unknown -1 Not yet resolved / floating Gray
Inactive 0 Logic LOW Dark green (theme-dependent)
Active 1 Logic HIGH Green (theme-dependent)
Error 2 Bus conflict / indeterminate Red

App/Core/StatusOps.h provides logic operations that respect these states with domination rules:

  • AND: Inactive dominates — AND(0, Unknown) = 0 because any false input makes the AND false.
  • OR: Active dominates — OR(1, Unknown) = 1 because any true input makes the OR true.
  • NOT: Inverts Active/Inactive; passes Unknown and Error through unchanged.
  • XOR: Requires all inputs to be known; any Unknown yields Unknown.

This matters for simulation correctness: a combinational gate can produce a determinate output even when some inputs are unconnected or unknown, which matches real-world digital logic behavior.

Strict vs. Permissive Input Updates

Elements choose which input-reading strategy to use:

  • simUpdateInputsAllowUnknown() (permissive) — Used by combinational gates. Allows Unknown/Error through so domination rules can resolve them. Only fails if a port is truly unconnected with no default.
  • simUpdateInputs() (strict) — Used by sequential elements. Any Unknown or Error input causes the element to propagate Unknown to all outputs and bail out. Sequential elements need complete, valid data.

Simulation Loop

The simulation (App/Simulation/Simulation.cpp) runs on a 1ms QTimer tick. Each tick executes these phases in order:

flowchart TD
    timer(["QTimer (1 ms)"])
    clocks["0. updateClock()<br/>Advance clock elements (real-time)"]
    step1["1. updateOutputs()<br/>Propagate user inputs: buttons, switches"]
    step2["2. updateLogic()<br/>Process elements in topological order:<br/>gates (zero-delay), flip-flops (edge)"]
    feedback{"Feedback loop?"}
    run_logic["Run updateLogic()<br/>on feedback elements"]
    settled{"Settled or<br/>iter >= 10?"}
    step3["3. updatePort()<br/>Propagate values via connections (out-&gt;in)"]
    step4["4. Update outputs<br/>LEDs, displays, buzzers redraw"]
    wait(["Wait for next tick (1 ms)"])

    timer --> clocks --> step1 --> step2 --> feedback
    feedback -- Yes --> run_logic --> settled
    settled -- No --> run_logic
    settled -- Yes --> step3
    feedback -- No --> step3
    step3 --> step4 --> wait
Loading

Topological Sorting

Before simulation starts, the engine builds a dependency graph from wire connections and computes a topological sort (App/Core/Priorities.h):

  • Each element is assigned a priority = length of the longest path from it to any sink (output), plus one.
  • Elements with higher priority are evaluated first (sources before sinks).
  • This guarantees that when an element's updateLogic() runs, all its predecessors have already been updated this cycle.
Input -> AND -> OR -> LED
  1st    2nd   3rd   4th

This is done during the initialize() phase, which builds the dependency graph from the scene's connections.


Simulation Wiring

During simulation initialization, physical wire connections are converted into a lightweight predecessor map:

struct SimInputConnection {
    GraphicElement *sourceElement = nullptr;
    int sourceOutputIndex = 0;
};

Each element stores a vector of these for its inputs. During updateLogic(), reading an input is just a direct pointer dereference — no graph traversal needed per tick.


Feedback Loop Handling

Some circuits have feedback (e.g., an SR latch where outputs feed back to inputs). The engine detects these using Tarjan's strongly connected components algorithm:

  1. Any element in a cycle is marked as a feedback node.
  2. Instead of a single-pass evaluation, the engine uses iterative settling: it runs updateLogic() on all elements, checks if any output changed, and repeats — up to 10 iterations or until convergence.
  3. If the circuit does not converge (e.g., a ring oscillator), a warning is emitted via the status bar.

See Also

Clone this wiki locally