From aa6d13a46c715c0b5079ecd8ae534b3a1a963190 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 26 Mar 2026 01:16:24 +0100 Subject: [PATCH] Add new cycle and event based simulator for better performance and associated unit tests --- StoneRed.LogicSimulator.Benchmarks/Program.cs | 5 + .../SimulatorBenchmarks.cs | 93 +++ .../StoneRed.LogicSimulator.Benchmarks.csproj | 16 + .../CircuitDefinition.cs | 90 ++- .../CycleCircuitSimulator.cs | 137 ++++ .../EventCircuitSimulator.cs | 142 ++++ .../GateKind.cs | 12 + .../ICircuitSimulator.cs | 29 + .../MacroInstance.cs | 3 + .../SimulatorBase.cs | 500 +++++++++++++ .../StoneRed.LogicSimulator.Simulation.csproj | 8 +- .../ExprCircuitSimulator.cs | 688 ------------------ StoneRed.LogicSimulator.Test/Program.cs | 56 -- .../AdvancedCircuitTests.cs | 201 +++++ .../SimulatorTests.cs | 157 ++++ .../StoneRed.LogicSimulator.Tests.csproj | 18 + StoneRed.LogicSimulator.sln | 24 +- 17 files changed, 1386 insertions(+), 793 deletions(-) create mode 100644 StoneRed.LogicSimulator.Benchmarks/Program.cs create mode 100644 StoneRed.LogicSimulator.Benchmarks/SimulatorBenchmarks.cs create mode 100644 StoneRed.LogicSimulator.Benchmarks/StoneRed.LogicSimulator.Benchmarks.csproj rename {StoneRed.LogicSimulator.Test => StoneRed.LogicSimulator.Simulation}/CircuitDefinition.cs (64%) create mode 100644 StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs create mode 100644 StoneRed.LogicSimulator.Simulation/EventCircuitSimulator.cs create mode 100644 StoneRed.LogicSimulator.Simulation/GateKind.cs create mode 100644 StoneRed.LogicSimulator.Simulation/ICircuitSimulator.cs create mode 100644 StoneRed.LogicSimulator.Simulation/MacroInstance.cs create mode 100644 StoneRed.LogicSimulator.Simulation/SimulatorBase.cs rename StoneRed.LogicSimulator.Test/StoneRed.LogicSimulator.Test.csproj => StoneRed.LogicSimulator.Simulation/StoneRed.LogicSimulator.Simulation.csproj (62%) delete mode 100644 StoneRed.LogicSimulator.Test/ExprCircuitSimulator.cs delete mode 100644 StoneRed.LogicSimulator.Test/Program.cs create mode 100644 StoneRed.LogicSimulator.Tests/AdvancedCircuitTests.cs create mode 100644 StoneRed.LogicSimulator.Tests/SimulatorTests.cs create mode 100644 StoneRed.LogicSimulator.Tests/StoneRed.LogicSimulator.Tests.csproj diff --git a/StoneRed.LogicSimulator.Benchmarks/Program.cs b/StoneRed.LogicSimulator.Benchmarks/Program.cs new file mode 100644 index 0000000..8d987f4 --- /dev/null +++ b/StoneRed.LogicSimulator.Benchmarks/Program.cs @@ -0,0 +1,5 @@ +using BenchmarkDotNet.Running; + +using StoneRed.LogicSimulator.Benchmarks; + +BenchmarkRunner.Run(); \ No newline at end of file diff --git a/StoneRed.LogicSimulator.Benchmarks/SimulatorBenchmarks.cs b/StoneRed.LogicSimulator.Benchmarks/SimulatorBenchmarks.cs new file mode 100644 index 0000000..e901c7d --- /dev/null +++ b/StoneRed.LogicSimulator.Benchmarks/SimulatorBenchmarks.cs @@ -0,0 +1,93 @@ +using BenchmarkDotNet.Attributes; +using StoneRed.LogicSimulator.Simulation; + +namespace StoneRed.LogicSimulator.Benchmarks; + +[MemoryDiagnoser] +public class SimulatorBenchmarks +{ + private CycleCircuitSimulator cycleSim = null!; + private EventCircuitSimulator eventSim = null!; + private int cycleSource; + private int eventSource; + private bool state; + + [Params(0.01, 0.10, 1.00)] + public double ActivityPercentage; + + [Params(true, false)] + public bool UseLut; + + [GlobalSetup] + public void Setup() + { + cycleSim = new CycleCircuitSimulator(); + eventSim = new EventCircuitSimulator(); + + SetupCircuit(cycleSim, out cycleSource); + SetupCircuit(eventSim, out eventSource); + } + + private void SetupCircuit(ICircuitSimulator sim, out int source) + { + var chain10 = new CircuitDefinition(); + int input = chain10.AddInputPin(); + int last = input; + for (int i = 0; i < 10; i++) + { + int not = chain10.AddGate(GateKind.Not); + chain10.Connect(last, not, 0); + last = not; + } + int output = chain10.AddOutputPin(); + chain10.Connect(last, output, 0); + + sim.RegisterMacroGate("CHAIN10", chain10); + if (UseLut) + { + sim.ComputeLut("CHAIN10"); + } + + const int macroCount = 100; + int activeMacroCount = Math.Max(1, (int)(macroCount * ActivityPercentage)); + int idleMacroCount = macroCount - activeMacroCount; + + source = sim.AddGate(GateKind.Source); + int constantSource = sim.AddGate(GateKind.Source); + int sink = sim.AddGate(GateKind.Sink); + + for (int i = 0; i < activeMacroCount; i++) + { + var inst = sim.AddMacroGate("CHAIN10"); + sim.ConnectGates(source, inst.Inputs[0], 0); + sim.ConnectGates(inst.Outputs[0], sink, 0); + } + + for (int i = 0; i < idleMacroCount; i++) + { + var inst = sim.AddMacroGate("CHAIN10"); + sim.ConnectGates(constantSource, inst.Inputs[0], 0); + sim.ConnectGates(inst.Outputs[0], sink, 0); + } + + sim.Reset(); + sim.SetSource(constantSource, false); + sim.RunUntilStable(); + } + + [Benchmark] + public void CycleBased() + { + state = !state; + cycleSim.SetSource(cycleSource, state); + cycleSim.Step(); + } + + [Benchmark] + public void EventDriven() + { + state = !state; + eventSim.SetSource(eventSource, state); + eventSim.Step(); + } +} diff --git a/StoneRed.LogicSimulator.Benchmarks/StoneRed.LogicSimulator.Benchmarks.csproj b/StoneRed.LogicSimulator.Benchmarks/StoneRed.LogicSimulator.Benchmarks.csproj new file mode 100644 index 0000000..7a52958 --- /dev/null +++ b/StoneRed.LogicSimulator.Benchmarks/StoneRed.LogicSimulator.Benchmarks.csproj @@ -0,0 +1,16 @@ + + + Exe + net8.0 + enable + enable + + + + + + + + + + diff --git a/StoneRed.LogicSimulator.Test/CircuitDefinition.cs b/StoneRed.LogicSimulator.Simulation/CircuitDefinition.cs similarity index 64% rename from StoneRed.LogicSimulator.Test/CircuitDefinition.cs rename to StoneRed.LogicSimulator.Simulation/CircuitDefinition.cs index dcb2b36..cf7fd24 100644 --- a/StoneRed.LogicSimulator.Test/CircuitDefinition.cs +++ b/StoneRed.LogicSimulator.Simulation/CircuitDefinition.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StoneRed.LogicSimulator.Test; +namespace StoneRed.LogicSimulator.Simulation; public sealed class CircuitDefinition { - private readonly List gateKinds = new(); - private readonly List<(int FromGate, int ToGate, byte ToInputBit)> connections = new(); - private readonly List inputPins = new(); - private readonly List outputPins = new(); - private readonly List macroInstances = new(); + private readonly List gateKinds = []; + private readonly List<(int FromGate, int ToGate, byte ToInputBit)> connections = []; + private readonly List inputPins = []; + private readonly List outputPins = []; + private readonly List macroInstances = []; public IReadOnlyList GateKinds => gateKinds; public IReadOnlyList<(int FromGate, int ToGate, byte ToInputBit)> Connections => connections; @@ -48,15 +44,9 @@ public sealed class CircuitDefinition throw new ArgumentException("Macro name must not be empty.", nameof(name)); } - if (inputCount < 0) - { - throw new ArgumentOutOfRangeException(nameof(inputCount)); - } + ArgumentOutOfRangeException.ThrowIfNegative(inputCount); - if (outputCount < 0) - { - throw new ArgumentOutOfRangeException(nameof(outputCount)); - } + ArgumentOutOfRangeException.ThrowIfNegative(outputCount); int[] inputs = new int[inputCount]; for (int i = 0; i < inputCount; i++) @@ -70,35 +60,41 @@ public sealed class CircuitDefinition outputs[i] = AddGate(GateKind.Sink); } - var instance = new MacroInstanceDef(name, inputs, outputs); + MacroInstanceDef instance = new MacroInstanceDef(name, inputs, outputs); macroInstances.Add(instance); return instance; } public void Connect(int fromGate, int toGate, int toInputBit) { - if ((uint)fromGate >= (uint)gateKinds.Count) throw new ArgumentOutOfRangeException(nameof(fromGate)); - if ((uint)toGate >= (uint)gateKinds.Count) throw new ArgumentOutOfRangeException(nameof(toGate)); - if ((uint)toInputBit >= 32u) throw new ArgumentOutOfRangeException(nameof(toInputBit)); + if ((uint)fromGate >= (uint)gateKinds.Count) + { + throw new ArgumentOutOfRangeException(nameof(fromGate)); + } + + if ((uint)toGate >= (uint)gateKinds.Count) + { + throw new ArgumentOutOfRangeException(nameof(toGate)); + } + + if ((uint)toInputBit >= 32u) + { + throw new ArgumentOutOfRangeException(nameof(toInputBit)); + } connections.Add((fromGate, toGate, (byte)toInputBit)); } public void Validate() { - // Minimal validation to prevent ambiguous semantics. - // - Input pins must be Source gates and must not have incoming connections. - // - Output pins must be Sink gates. - // - All Source gates must be listed as InputPins. - // - LUT gates are not allowed in definitions (they are generated by the simulator). - var hasIncoming = new bool[gateKinds.Count]; + bool[] hasIncoming = new bool[gateKinds.Count]; for (int i = 0; i < connections.Count; i++) { (_, int to, _) = connections[i]; hasIncoming[to] = true; } - var inputPinSet = new HashSet(inputPins); + HashSet inputPinSet = [.. inputPins]; for (int i = 0; i < gateKinds.Count; i++) { if (gateKinds[i] == GateKind.Source && !inputPinSet.Contains(i)) @@ -112,7 +108,7 @@ public sealed class CircuitDefinition } } - var macroPinGates = new HashSet(); + HashSet macroPinGates = []; foreach (MacroInstanceDef instance in macroInstances) { if (string.IsNullOrWhiteSpace(instance.Name)) @@ -122,16 +118,38 @@ public sealed class CircuitDefinition foreach (int pin in instance.Inputs) { - if ((uint)pin >= (uint)gateKinds.Count) throw new InvalidOperationException("Macro instance input pin is out of range."); - if (gateKinds[pin] != GateKind.Buffer) throw new InvalidOperationException("Macro instance inputs must be Buffer gates."); - if (!macroPinGates.Add(pin)) throw new InvalidOperationException("Macro instance pin is used more than once."); + if ((uint)pin >= (uint)gateKinds.Count) + { + throw new InvalidOperationException("Macro instance input pin is out of range."); + } + + if (gateKinds[pin] != GateKind.Buffer) + { + throw new InvalidOperationException("Macro instance inputs must be Buffer gates."); + } + + if (!macroPinGates.Add(pin)) + { + throw new InvalidOperationException("Macro instance pin is used more than once."); + } } foreach (int pin in instance.Outputs) { - if ((uint)pin >= (uint)gateKinds.Count) throw new InvalidOperationException("Macro instance output pin is out of range."); - if (gateKinds[pin] != GateKind.Sink) throw new InvalidOperationException("Macro instance outputs must be Sink gates."); - if (!macroPinGates.Add(pin)) throw new InvalidOperationException("Macro instance pin is used more than once."); + if ((uint)pin >= (uint)gateKinds.Count) + { + throw new InvalidOperationException("Macro instance output pin is out of range."); + } + + if (gateKinds[pin] != GateKind.Sink) + { + throw new InvalidOperationException("Macro instance outputs must be Sink gates."); + } + + if (!macroPinGates.Add(pin)) + { + throw new InvalidOperationException("Macro instance pin is used more than once."); + } } } diff --git a/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs b/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs new file mode 100644 index 0000000..68b58f5 --- /dev/null +++ b/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs @@ -0,0 +1,137 @@ +using System.Linq.Expressions; + +namespace StoneRed.LogicSimulator.Simulation; + +public sealed class CycleCircuitSimulator : SimulatorBase +{ + private int[] nextInputMasks = []; + private Action computeOutputs = (_, _, _) => { }; + + protected override void EnsureStorage() + { + base.EnsureStorage(); + if (nextInputMasks.Length != gateKinds.Count) + { + nextInputMasks = new int[gateKinds.Count]; + } + } + + public override void Reset() + { + base.Reset(); + Array.Clear(nextInputMasks); + } + + protected override void OnSourceChanged(int gateId) { } + + public override void Step() + { + EnsureCompiled(); + if (!initialized) + { + Reset(); + } + + computeOutputs(inputMasks, outputMasks, sourceStates); + PropagateAndSwap(); + NotifyAllWatchers(); + } + + private void PropagateAndSwap() + { + Array.Clear(nextInputMasks); + for (int fromGate = 0; fromGate < gateKinds.Count; fromGate++) + { + if ((outputMasks[fromGate] & 1) == 0) + { + continue; + } + + int start = edgeStart[fromGate]; + int end = edgeStart[fromGate + 1]; + for (int e = start; e < end; e++) + { + nextInputMasks[edgeToGate[e]] |= 1 << edgeToInputBit[e]; + } + } + (inputMasks, nextInputMasks) = (nextInputMasks, inputMasks); + } + + private bool PropagateAndSwapDetectChange() + { + Array.Clear(nextInputMasks); + for (int fromGate = 0; fromGate < gateKinds.Count; fromGate++) + { + if ((outputMasks[fromGate] & 1) == 0) + { + continue; + } + + int start = edgeStart[fromGate]; + int end = edgeStart[fromGate + 1]; + for (int e = start; e < end; e++) + { + nextInputMasks[edgeToGate[e]] |= 1 << edgeToInputBit[e]; + } + } + + bool changed = false; + for (int i = 0; i < inputMasks.Length; i++) + { + if (inputMasks[i] != nextInputMasks[i]) { changed = true; break; } + } + + (inputMasks, nextInputMasks) = (nextInputMasks, inputMasks); + return changed; + } + + public override bool TryRunUntilStable(int maxSteps, out int steps) + { + steps = 0; + if (maxSteps <= 0) + { + return false; + } + + EnsureCompiled(); + if (!initialized) + { + Reset(); + } + + bool changed = true; + while (changed && steps < maxSteps) + { + steps++; + computeOutputs(inputMasks, outputMasks, sourceStates); + changed = PropagateAndSwapDetectChange(); + NotifyAllWatchers(); + } + return !changed; + } + + protected override void CompileEngine() + { + ParameterExpression inputsParam = Expression.Parameter(typeof(int[]), "inputs"); + ParameterExpression outputsParam = Expression.Parameter(typeof(int[]), "outputs"); + ParameterExpression sourcesParam = Expression.Parameter(typeof(int[]), "sources"); + ConstantExpression lutDataConst = Expression.Constant(lutData); + + List body = []; + for (int i = 0; i < gateKinds.Count; i++) + { + ConstantExpression indexExpr = Expression.Constant(i); + BinaryExpression inMask = Expression.ArrayIndex(inputsParam, indexExpr); + Expression logicExpr = GenerateGateLogic(i, inMask, sourcesParam, indexExpr, lutDataConst); + + body.Add(Expression.Assign(Expression.ArrayAccess(outputsParam, indexExpr), logicExpr)); + } + + computeOutputs = Expression.Lambda>(Expression.Block(body), inputsParam, outputsParam, sourcesParam).Compile(); + } + + protected override SimulatorBase CreateInternalSimulator() + { + return new CycleCircuitSimulator(); + } +} diff --git a/StoneRed.LogicSimulator.Simulation/EventCircuitSimulator.cs b/StoneRed.LogicSimulator.Simulation/EventCircuitSimulator.cs new file mode 100644 index 0000000..b6e1ce7 --- /dev/null +++ b/StoneRed.LogicSimulator.Simulation/EventCircuitSimulator.cs @@ -0,0 +1,142 @@ +using System.Linq.Expressions; + +namespace StoneRed.LogicSimulator.Simulation; + +public sealed class EventCircuitSimulator : SimulatorBase +{ + private Action[] gateEvaluators = []; + private readonly Queue activeQueue = new(); + private bool[] inQueue = []; + + protected override void OnGateAdded(int gateId) + { + if (inQueue.Length != gateKinds.Count) + { + Array.Resize(ref inQueue, gateKinds.Count); + } + } + + public override void Reset() + { + base.Reset(); + activeQueue.Clear(); + Array.Clear(inQueue); + for (int i = 0; i < gateKinds.Count; i++) + { + Enqueue(i); + } + } + + protected override void OnSourceChanged(int gateId) + { + Enqueue(gateId); + } + + private void Enqueue(int gateId) + { + if (!inQueue[gateId]) + { + inQueue[gateId] = true; + activeQueue.Enqueue(gateId); + } + } + + public override void Step() + { + EnsureCompiled(); + if (!initialized) + { + Reset(); + } + + while (activeQueue.Count > 0) + { + int gateId = activeQueue.Dequeue(); + inQueue[gateId] = false; + + int oldOutput = outputMasks[gateId]; + gateEvaluators[gateId](inputMasks, outputMasks, sourceStates, gateId); + + if (outputMasks[gateId] != oldOutput) + { + Propagate(gateId); + NotifyGateWatchers(gateId); + } + } + } + + private void Propagate(int fromGate) + { + int start = edgeStart[fromGate]; + int end = edgeStart[fromGate + 1]; + int outVal = outputMasks[fromGate] & 1; + + for (int e = start; e < end; e++) + { + int toGate = edgeToGate[e]; + int bit = edgeToInputBit[e]; + int oldBit = (inputMasks[toGate] >> bit) & 1; + if (oldBit != outVal) + { + inputMasks[toGate] ^= 1 << bit; + Enqueue(toGate); + } + } + } + + public override bool TryRunUntilStable(int maxSteps, out int steps) + { + steps = 0; + if (maxSteps <= 0) + { + return false; + } + + EnsureCompiled(); + if (!initialized) + { + Reset(); + } + + while (activeQueue.Count > 0 && steps < maxSteps) + { + steps++; + Step(); + } + return activeQueue.Count == 0; + } + + protected override void CompileEngine() + { + gateEvaluators = new Action[gateKinds.Count]; + ParameterExpression inputsParam = Expression.Parameter(typeof(int[]), "inputs"); + ParameterExpression outputsParam = Expression.Parameter(typeof(int[]), "outputs"); + ParameterExpression sourcesParam = Expression.Parameter(typeof(int[]), "sources"); + ParameterExpression gateIdParam = Expression.Parameter(typeof(int), "gateId"); + ConstantExpression lutDataConst = Expression.Constant(lutData); + + for (int i = 0; i < gateKinds.Count; i++) + { + BinaryExpression inMask = Expression.ArrayIndex(inputsParam, gateIdParam); + Expression logicExpr = GenerateGateLogic(i, inMask, sourcesParam, gateIdParam, lutDataConst); + + gateEvaluators[i] = Expression.Lambda>( + Expression.Assign(Expression.ArrayAccess(outputsParam, gateIdParam), logicExpr), + inputsParam, outputsParam, sourcesParam, gateIdParam).Compile(); + } + } + + protected override SimulatorBase CreateInternalSimulator() + { + return new EventCircuitSimulator(); + } + + protected override void EnsureStorage() + { + base.EnsureStorage(); + if (inQueue.Length != gateKinds.Count) + { + Array.Resize(ref inQueue, gateKinds.Count); + } + } +} diff --git a/StoneRed.LogicSimulator.Simulation/GateKind.cs b/StoneRed.LogicSimulator.Simulation/GateKind.cs new file mode 100644 index 0000000..eb0e5f3 --- /dev/null +++ b/StoneRed.LogicSimulator.Simulation/GateKind.cs @@ -0,0 +1,12 @@ +namespace StoneRed.LogicSimulator.Simulation; + +public enum GateKind : byte +{ + Source, + Not, + And2, + Or2, + Buffer, + Sink, + Lut, +} diff --git a/StoneRed.LogicSimulator.Simulation/ICircuitSimulator.cs b/StoneRed.LogicSimulator.Simulation/ICircuitSimulator.cs new file mode 100644 index 0000000..f7ea9c5 --- /dev/null +++ b/StoneRed.LogicSimulator.Simulation/ICircuitSimulator.cs @@ -0,0 +1,29 @@ +namespace StoneRed.LogicSimulator.Simulation; + +/// +/// Represents a digital logic simulator. +/// +public interface ICircuitSimulator +{ + int GateCount { get; } + int AddGate(GateKind kind); + int AddLutGate(int inputCount, int[] table); + void ConnectGates(int fromGate, int toGate, int toInputBit); + void RegisterMacroGate(string name, CircuitDefinition definition); + bool ComputeLut(string name, int maxSteps = 4096); + MacroInstance AddMacroGate(string name); + + /// + /// Returns the circuit to its initial state (all signals at 0) and + /// kickstarts the simulation logic (evaluating gates like NOT). + /// This is automatically called on the first Step if not called manually. + /// + void Reset(); + + void SetSource(int gateId, bool value); + bool GetOutput(int gateId); + void Step(); + int RunUntilStable(int maxSteps = 1024); + bool TryRunUntilStable(int maxSteps, out int steps); + IDisposable WatchGate(int gateId, Action callback); +} diff --git a/StoneRed.LogicSimulator.Simulation/MacroInstance.cs b/StoneRed.LogicSimulator.Simulation/MacroInstance.cs new file mode 100644 index 0000000..d6a761c --- /dev/null +++ b/StoneRed.LogicSimulator.Simulation/MacroInstance.cs @@ -0,0 +1,3 @@ +namespace StoneRed.LogicSimulator.Simulation; + +public sealed record MacroInstance(string Name, int[] Inputs, int[] Outputs); diff --git a/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs b/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs new file mode 100644 index 0000000..2fcf782 --- /dev/null +++ b/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs @@ -0,0 +1,500 @@ +using System.Linq.Expressions; + +namespace StoneRed.LogicSimulator.Simulation; + +public abstract class SimulatorBase : ICircuitSimulator +{ + protected readonly List gateKinds = []; + protected readonly List<(int FromGate, int ToGate, byte ToInputBit)> connections = []; + protected readonly List lutTableByGate = []; + protected readonly Dictionary macroGates = new(StringComparer.Ordinal); + + protected int[] inputMasks = []; + protected int[] outputMasks = []; + protected int[] sourceStates = []; + protected bool[] sourceInitialized = []; + + protected int[] edgeStart = []; + protected int[] edgeToGate = []; + protected byte[] edgeToInputBit = []; + + protected int[] lutOffsets = []; + protected int[] lutMasks = []; + protected int[] lutData = []; + + protected bool compiled; + protected bool initialized; + + private readonly List allWatchers = []; + private Action[][] watcherCache = []; + private int[] gatesWithWatchers = []; + protected int nextWatcherId; + + protected sealed record GateWatcherEntry(int Id, int GateId, Action Callback); + protected sealed record MacroLut(int InputCount, int OutputCount, int[][] OutputTables); + protected sealed record MacroInfo(CircuitDefinition Definition, MacroLut? Lut); + + public int GateCount => gateKinds.Count; + + public int AddGate(GateKind kind) + { + if (kind == GateKind.Lut) + { + throw new InvalidOperationException("Use AddLutGate() to create LUT gates."); + } + + int id = gateKinds.Count; + gateKinds.Add(kind); + lutTableByGate.Add(null); + OnGateAdded(id); + compiled = false; + initialized = false; + return id; + } + + protected virtual void OnGateAdded(int gateId) { } + + public int AddLutGate(int inputCount, int[] table) + { + if (inputCount is < 0 or > 30) + { + throw new ArgumentOutOfRangeException(nameof(inputCount)); + } + + ArgumentNullException.ThrowIfNull(table); + if (table.Length != (1 << inputCount)) + { + throw new ArgumentException("Invalid table length."); + } + + int id = gateKinds.Count; + gateKinds.Add(GateKind.Lut); + lutTableByGate.Add(table); + OnGateAdded(id); + compiled = false; + initialized = false; + return id; + } + + public void ConnectGates(int fromGate, int toGate, int toInputBit) + { + if ((uint)fromGate >= (uint)gateKinds.Count) + { + throw new ArgumentOutOfRangeException(nameof(fromGate)); + } + + if ((uint)toGate >= (uint)gateKinds.Count) + { + throw new ArgumentOutOfRangeException(nameof(toGate)); + } + + if ((uint)toInputBit >= 32u) + { + throw new ArgumentOutOfRangeException(nameof(toInputBit)); + } + + connections.Add((fromGate, toGate, (byte)toInputBit)); + compiled = false; + initialized = false; + } + + public void RegisterMacroGate(string name, CircuitDefinition definition) + { + definition.Validate(); + macroGates[name] = new MacroInfo(definition, Lut: null); + compiled = false; + initialized = false; + } + + public virtual void Reset() + { + EnsureStorage(); + Array.Clear(inputMasks); + Array.Clear(outputMasks); + Array.Clear(sourceInitialized); + initialized = true; + } + + public abstract void Step(); + public abstract bool TryRunUntilStable(int maxSteps, out int steps); + + public int RunUntilStable(int maxSteps = 1024) + { + if (!TryRunUntilStable(maxSteps, out int steps)) + { + throw new InvalidOperationException($"Circuit did not stabilize within {maxSteps} steps."); + } + return steps; + } + + public virtual void SetSource(int gateId, bool value) + { + EnsureStorage(); + if ((uint)gateId >= (uint)gateKinds.Count) + { + throw new ArgumentOutOfRangeException(nameof(gateId)); + } + + if (gateKinds[gateId] != GateKind.Source) + { + throw new InvalidOperationException("Gate is not a source."); + } + + int bit = value ? 1 : 0; + if (sourceStates[gateId] != bit || !sourceInitialized[gateId]) + { + sourceStates[gateId] = bit; + sourceInitialized[gateId] = true; + OnSourceChanged(gateId); + } + } + + protected virtual void OnSourceChanged(int gateId) { } + + public bool GetOutput(int gateId) + { + EnsureStorage(); + return (outputMasks[gateId] & 1) != 0; + } + + public IDisposable WatchGate(int gateId, Action callback) + { + ArgumentNullException.ThrowIfNull(callback); + if ((uint)gateId >= (uint)gateKinds.Count) + { + throw new ArgumentOutOfRangeException(nameof(gateId)); + } + + int id = nextWatcherId++; + GateWatcherEntry entry = new GateWatcherEntry(id, gateId, callback); + allWatchers.Add(entry); + RebuildWatcherCache(); + return new GateWatcherSubscription(this, id); + } + + private void RemoveWatcher(int id) + { + _ = allWatchers.RemoveAll(w => w.Id == id); + RebuildWatcherCache(); + } + + private void RebuildWatcherCache() + { + int n = gateKinds.Count; + watcherCache = new Action[n][]; + + IEnumerable> groups = allWatchers.GroupBy(w => w.GateId); + List activeGates = []; + + foreach (IGrouping group in groups) + { + watcherCache[group.Key] = [.. group.Select(w => w.Callback)]; + activeGates.Add(group.Key); + } + + gatesWithWatchers = [.. activeGates]; + } + + protected void NotifyAllWatchers() + { + for (int i = 0; i < gatesWithWatchers.Length; i++) + { + int gateId = gatesWithWatchers[i]; + Action[] callbacks = watcherCache[gateId]; + int val = outputMasks[gateId]; + for (int j = 0; j < callbacks.Length; j++) + { + callbacks[j](gateId, val); + } + } + } + + protected void NotifyGateWatchers(int gateId) + { + if (gateId >= watcherCache.Length) + { + return; + } + + Action[] callbacks = watcherCache[gateId]; + if (callbacks == null) + { + return; + } + + int val = outputMasks[gateId]; + for (int i = 0; i < callbacks.Length; i++) + { + callbacks[i](gateId, val); + } + } + + protected virtual void EnsureStorage() + { + int n = gateKinds.Count; + if (inputMasks.Length == n) + { + return; + } + + inputMasks = new int[n]; + outputMasks = new int[n]; + sourceStates = new int[n]; + sourceInitialized = new bool[n]; + } + + protected void EnsureCompiled() + { + EnsureStorage(); + if (compiled) + { + return; + } + + CompileNetlist(); + CompileLuts(); + CompileEngine(); + compiled = true; + } + + protected abstract void CompileEngine(); + + protected Expression GenerateGateLogic( + int gateId, + Expression inMask, + Expression sourcesParam, + Expression indexExpr, + Expression lutDataConst) + { + return gateKinds[gateId] switch + { + GateKind.Source => Expression.And(Expression.ArrayIndex(sourcesParam, indexExpr), Expression.Constant(1)), + GateKind.Not => Expression.Condition(Expression.Equal(Expression.And(inMask, Expression.Constant(1)), Expression.Constant(0)), Expression.Constant(1), Expression.Constant(0)), + GateKind.And2 => Expression.Condition(Expression.Equal(Expression.And(inMask, Expression.Constant(0b11)), Expression.Constant(0b11)), Expression.Constant(1), Expression.Constant(0)), + GateKind.Or2 => Expression.Condition(Expression.NotEqual(Expression.And(inMask, Expression.Constant(0b11)), Expression.Constant(0)), Expression.Constant(1), Expression.Constant(0)), + GateKind.Buffer => Expression.Condition(Expression.NotEqual(Expression.And(inMask, Expression.Constant(1)), Expression.Constant(0)), Expression.Constant(1), Expression.Constant(0)), + GateKind.Sink => Expression.Condition(Expression.NotEqual(Expression.And(inMask, Expression.Constant(1)), Expression.Constant(0)), Expression.Constant(1), Expression.Constant(0)), + GateKind.Lut => Expression.ArrayIndex(lutDataConst, Expression.Add(Expression.Constant(lutOffsets[gateId]), Expression.And(inMask, Expression.Constant(lutMasks[gateId])))), + _ => throw new ArgumentOutOfRangeException(), + }; + } + + private void CompileNetlist() + { + int n = gateKinds.Count; + edgeStart = new int[n + 1]; + foreach ((int FromGate, int _, byte _) in connections) + { + edgeStart[FromGate + 1]++; + } + + for (int i = 1; i < edgeStart.Length; i++) + { + edgeStart[i] += edgeStart[i - 1]; + } + + edgeToGate = new int[connections.Count]; + edgeToInputBit = new byte[connections.Count]; + int[] cursor = (int[])edgeStart.Clone(); + foreach ((int FromGate, int ToGate, byte ToInputBit) in connections) + { + int at = cursor[FromGate]++; + edgeToGate[at] = ToGate; + edgeToInputBit[at] = ToInputBit; + } + } + + private void CompileLuts() + { + int n = gateKinds.Count; + lutOffsets = new int[n]; + lutMasks = new int[n]; + int total = 0; + for (int i = 0; i < n; i++) + { + if (gateKinds[i] != GateKind.Lut) + { + continue; + } + + int[] table = lutTableByGate[i]!; + lutOffsets[i] = total; + lutMasks[i] = table.Length - 1; + total += table.Length; + } + lutData = new int[total]; + int cursor = 0; + for (int i = 0; i < n; i++) + { + if (gateKinds[i] != GateKind.Lut) + { + continue; + } + + int[] table = lutTableByGate[i]!; + Array.Copy(table, 0, lutData, cursor, table.Length); + cursor += table.Length; + } + } + + public bool ComputeLut(string name, int maxSteps = 4096) + { + if (!macroGates.TryGetValue(name, out MacroInfo? macro)) + { + throw new KeyNotFoundException(); + } + + MacroLut? lut = TryBuildMacroLut(macro.Definition, maxSteps); + macroGates[name] = macro with { Lut = lut }; + compiled = false; + initialized = false; + return lut is not null; + } + + protected abstract SimulatorBase CreateInternalSimulator(); + + private MacroLut? TryBuildMacroLut(CircuitDefinition definition, int maxSteps) + { + int inputCount = definition.InputPins.Count; + int outputCount = definition.OutputPins.Count; + if (inputCount < 0 || outputCount <= 0 || inputCount > 30) + { + return null; + } + + int patterns = 1 << inputCount; + SimulatorBase sim = CreateInternalSimulator(); + foreach (KeyValuePair pair in macroGates) + { + sim.macroGates[pair.Key] = pair.Value; + } + + int[] map = CopyDefinitionGatesAndConnections(sim, definition, static (_, kind) => kind); + int[] inGates = MapPins(definition.InputPins, map); + int[] outGates = MapPins(definition.OutputPins, map); + + int[][] outputTables = new int[outputCount][]; + for (int o = 0; o < outputCount; o++) + { + outputTables[o] = new int[patterns]; + } + + for (int pattern = 0; pattern < patterns; pattern++) + { + sim.Reset(); + for (int i = 0; i < inputCount; i++) + { + sim.SetSource(inGates[i], ((pattern >> i) & 1) != 0); + } + + if (!sim.TryRunUntilStable(maxSteps, out _)) + { + return null; + } + + for (int o = 0; o < outputCount; o++) + { + outputTables[o][pattern] = sim.GetOutput(outGates[o]) ? 1 : 0; + } + } + return new MacroLut(inputCount, outputCount, outputTables); + } + + public MacroInstance AddMacroGate(string name) + { + if (!macroGates.TryGetValue(name, out MacroInfo? macro)) + { + throw new KeyNotFoundException(); + } + + if (macro.Lut is not null) + { + return AddMacroGateFromLut(name, macro.Lut); + } + + int[] map = CopyDefinitionGatesAndConnections(this, macro.Definition, (gateId, kind) => + kind == GateKind.Source ? (macro.Definition.InputPins.Contains(gateId) ? GateKind.Buffer : throw new InvalidOperationException()) : kind); + + return new MacroInstance(name, MapPins(macro.Definition.InputPins, map), MapPins(macro.Definition.OutputPins, map)); + } + + private MacroInstance AddMacroGateFromLut(string name, MacroLut lut) + { + int[] inputs = new int[lut.InputCount]; + for (int i = 0; i < inputs.Length; i++) + { + inputs[i] = AddGate(GateKind.Buffer); + } + + int[] outputs = new int[lut.OutputCount]; + for (int o = 0; o < outputs.Length; o++) + { + int lutGate = AddLutGate(lut.InputCount, lut.OutputTables[o]); + for (int i = 0; i < inputs.Length; i++) + { + ConnectGates(inputs[i], lutGate, i); + } + + int sink = AddGate(GateKind.Sink); + ConnectGates(lutGate, sink, 0); + outputs[o] = sink; + } + return new MacroInstance(name, inputs, outputs); + } + + protected static int[] CopyDefinitionGatesAndConnections(SimulatorBase destination, CircuitDefinition definition, Func mapKind) + { + int gateCount = definition.GateKinds.Count; + int[] map = new int[gateCount]; + Array.Fill(map, -1); + + for (int i = 0; i < definition.MacroInstances.Count; i++) + { + CircuitDefinition.MacroInstanceDef instanceDef = definition.MacroInstances[i]; + MacroInstance instance = destination.AddMacroGate(instanceDef.Name); + for (int p = 0; p < instanceDef.Inputs.Length; p++) + { + map[instanceDef.Inputs[p]] = instance.Inputs[p]; + } + + for (int p = 0; p < instanceDef.Outputs.Length; p++) + { + map[instanceDef.Outputs[p]] = instance.Outputs[p]; + } + } + + for (int i = 0; i < gateCount; i++) + { + if (map[i] == -1) + { + map[i] = destination.AddGate(mapKind(i, definition.GateKinds[i])); + } + } + + foreach ((int FromGate, int ToGate, byte ToInputBit) in definition.Connections) + { + destination.ConnectGates(map[FromGate], map[ToGate], ToInputBit); + } + + return map; + } + + protected static int[] MapPins(IReadOnlyList pins, int[] map) + { + int[] result = new int[pins.Count]; + for (int i = 0; i < result.Length; i++) + { + result[i] = map[pins[i]]; + } + + return result; + } + + private sealed class GateWatcherSubscription(SimulatorBase simulator, int id) : IDisposable + { + public void Dispose() + { + simulator.RemoveWatcher(id); + } + } +} diff --git a/StoneRed.LogicSimulator.Test/StoneRed.LogicSimulator.Test.csproj b/StoneRed.LogicSimulator.Simulation/StoneRed.LogicSimulator.Simulation.csproj similarity index 62% rename from StoneRed.LogicSimulator.Test/StoneRed.LogicSimulator.Test.csproj rename to StoneRed.LogicSimulator.Simulation/StoneRed.LogicSimulator.Simulation.csproj index 129061d..e8cd599 100644 --- a/StoneRed.LogicSimulator.Test/StoneRed.LogicSimulator.Test.csproj +++ b/StoneRed.LogicSimulator.Simulation/StoneRed.LogicSimulator.Simulation.csproj @@ -1,13 +1,7 @@ - - + - Exe net8.0 enable enable - - - - diff --git a/StoneRed.LogicSimulator.Test/ExprCircuitSimulator.cs b/StoneRed.LogicSimulator.Test/ExprCircuitSimulator.cs deleted file mode 100644 index ccfbd7b..0000000 --- a/StoneRed.LogicSimulator.Test/ExprCircuitSimulator.cs +++ /dev/null @@ -1,688 +0,0 @@ -using System; -using System.Linq.Expressions; - -namespace StoneRed.LogicSimulator.Test; - -public enum GateKind : byte -{ - Source, - Not, - And2, - Or2, - Buffer, - Sink, - Lut, -} - -public sealed class ExprCircuitSimulator -{ - private readonly List gateKinds = []; - private readonly List<(int FromGate, int ToGate, byte ToInputBit)> connections = []; - private readonly List lutTableByGate = []; - private readonly Dictionary macroGates = new(StringComparer.Ordinal); - - private int[] inputMasks = Array.Empty(); - private int[] nextInputMasks = Array.Empty(); - private int[] outputMasks = Array.Empty(); - private int[] sourceStates = Array.Empty(); - - private int[] edgeStart = Array.Empty(); - private int[] edgeToGate = Array.Empty(); - private byte[] edgeToInputBit = Array.Empty(); - - private int[] lutOffsets = Array.Empty(); - private int[] lutMasks = Array.Empty(); - private int[] lutData = Array.Empty(); - - private Action? computeOutputs; - private bool compiled; - private readonly List gateWatchers = []; - private int nextWatcherId; - - private sealed record GateWatcherEntry(int Id, int GateId, Action Callback); - - public int GateCount => gateKinds.Count; - - public sealed record MacroInstance(string Name, int[] Inputs, int[] Outputs); - - private sealed record MacroLut(int InputCount, int OutputCount, int[][] OutputTables); - - private sealed record MacroInfo(CircuitDefinition Definition, MacroLut? Lut); - - public int AddGate(GateKind kind) - { - if (kind == GateKind.Lut) - { - throw new InvalidOperationException("Use AddLutGate() to create LUT gates."); - } - - int id = gateKinds.Count; - gateKinds.Add(kind); - lutTableByGate.Add(null); - compiled = false; - return id; - } - - public int AddLutGate(int inputCount, int[] table) - { - if (inputCount is < 0 or > 30) - { - throw new ArgumentOutOfRangeException(nameof(inputCount), "LUT input count must be between 0 and 30."); - } - - if (table is null) - { - throw new ArgumentNullException(nameof(table)); - } - - int expected = 1 << inputCount; - if (table.Length != expected) - { - throw new ArgumentException($"LUT table length must be {expected} for {inputCount} inputs.", nameof(table)); - } - - int id = gateKinds.Count; - gateKinds.Add(GateKind.Lut); - lutTableByGate.Add(table); - compiled = false; - return id; - } - - public void ConnectGates(int fromGate, int toGate, int toInputBit) - { - if ((uint)fromGate >= (uint)gateKinds.Count) - { - throw new ArgumentOutOfRangeException(nameof(fromGate)); - } - - if ((uint)toGate >= (uint)gateKinds.Count) - { - throw new ArgumentOutOfRangeException(nameof(toGate)); - } - - if ((uint)toInputBit >= 32u) - { - throw new ArgumentOutOfRangeException(nameof(toInputBit)); - } - - connections.Add((fromGate, toGate, (byte)toInputBit)); - compiled = false; - } - - public void RegisterMacroGate(string name, CircuitDefinition definition) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Macro name must not be empty.", nameof(name)); - } - - definition.Validate(); - macroGates[name] = new MacroInfo(definition, Lut: null); - compiled = false; - } - - public bool ComputeLut(string name, int maxSteps = 4096) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Macro name must not be empty.", nameof(name)); - } - - if (maxSteps <= 0) - { - throw new ArgumentOutOfRangeException(nameof(maxSteps)); - } - - if (!macroGates.TryGetValue(name, out MacroInfo? macro)) - { - throw new KeyNotFoundException($"Macro gate '{name}' is not registered."); - } - - MacroLut? lut = TryBuildMacroLut(macro.Definition, maxSteps: maxSteps); - macroGates[name] = macro with { Lut = lut }; - compiled = false; - return lut is not null; - } - - public MacroInstance AddMacroGate(string name) - { - if (!macroGates.TryGetValue(name, out MacroInfo? macro)) - { - throw new KeyNotFoundException($"Macro gate '{name}' is not registered."); - } - - CircuitDefinition definition = macro.Definition; - - if (macro.Lut is not null) - { - return AddMacroGateFromLut(name, macro.Lut); - } - - // Inline (flatten) definition into this simulator by copying its gates and connections. - // Input pins are represented as Source gates in the definition, but Source gates cannot be driven by wires. - // For each input pin we substitute a Buffer gate, which can be driven externally and fans out internally. - int[] map = CopyDefinitionGatesAndConnections( - destination: this, - definition: definition, - mapKind: (gateId, kind) => - kind == GateKind.Source - ? (definition.InputPins.Contains(gateId) ? GateKind.Buffer : throw new InvalidOperationException("Only Source gates marked as InputPins are allowed inside a macro definition.")) - : kind); - - int[] inputs = MapPins(definition.InputPins, map); - int[] outputs = MapPins(definition.OutputPins, map); - - return new MacroInstance(name, inputs, outputs); - } - - public void ClearSignals() - { - EnsureStorage(); - Array.Clear(inputMasks); - Array.Clear(nextInputMasks); - Array.Clear(outputMasks); - } - - public void SetSource(int gateId, bool value) - { - EnsureStorage(); - - if ((uint)gateId >= (uint)gateKinds.Count) - { - throw new ArgumentOutOfRangeException(nameof(gateId)); - } - - if (gateKinds[gateId] != GateKind.Source) - { - throw new InvalidOperationException("Gate is not a source."); - } - - sourceStates[gateId] = value ? 1 : 0; - } - - public bool GetOutput(int gateId) - { - EnsureStorage(); - if ((uint)gateId >= (uint)gateKinds.Count) - { - throw new ArgumentOutOfRangeException(nameof(gateId)); - } - - return (outputMasks[gateId] & 1) != 0; - } - - public void Step() - { - EnsureCompiled(); - computeOutputs!(inputMasks, outputMasks, sourceStates); - Propagate(); - NotifyWatchers(); - } - - public int RunUntilStable(int maxSteps = 1024) - { - if (!TryRunUntilStable(maxSteps, out int steps)) - { - throw new InvalidOperationException($"Circuit did not stabilize within {maxSteps} steps."); - } - - return steps; - } - - private void Propagate() - { - Array.Clear(nextInputMasks); - - for (int fromGate = 0; fromGate < gateKinds.Count; fromGate++) - { - if ((outputMasks[fromGate] & 1) == 0) - { - continue; - } - - int start = edgeStart[fromGate]; - int end = edgeStart[fromGate + 1]; - - for (int e = start; e < end; e++) - { - int toGate = edgeToGate[e]; - nextInputMasks[toGate] |= 1 << edgeToInputBit[e]; - } - } - - (inputMasks, nextInputMasks) = (nextInputMasks, inputMasks); - } - - private bool PropagateAndSwapDetectChange() - { - Array.Clear(nextInputMasks); - - for (int fromGate = 0; fromGate < gateKinds.Count; fromGate++) - { - if ((outputMasks[fromGate] & 1) == 0) - { - continue; - } - - int start = edgeStart[fromGate]; - int end = edgeStart[fromGate + 1]; - - for (int e = start; e < end; e++) - { - int toGate = edgeToGate[e]; - nextInputMasks[toGate] |= 1 << edgeToInputBit[e]; - } - } - - bool changed = false; - for (int i = 0; i < inputMasks.Length; i++) - { - if (inputMasks[i] != nextInputMasks[i]) - { - changed = true; - break; - } - } - - (inputMasks, nextInputMasks) = (nextInputMasks, inputMasks); - return changed; - } - - private void EnsureStorage() - { - int n = gateKinds.Count; - if (inputMasks.Length == n) - { - return; - } - - inputMasks = new int[n]; - nextInputMasks = new int[n]; - outputMasks = new int[n]; - sourceStates = new int[n]; - } - - private void EnsureCompiled() - { - EnsureStorage(); - - if (compiled) - { - return; - } - - CompileNetlist(); - CompileLuts(); - computeOutputs = CompileComputeOutputsExpr(); - compiled = true; - } - - private void CompileNetlist() - { - int n = gateKinds.Count; - edgeStart = new int[n + 1]; - - for (int i = 0; i < connections.Count; i++) - { - (int from, _, _) = connections[i]; - edgeStart[from + 1]++; - } - - for (int i = 1; i < edgeStart.Length; i++) - { - edgeStart[i] += edgeStart[i - 1]; - } - - edgeToGate = new int[connections.Count]; - edgeToInputBit = new byte[connections.Count]; - - int[] cursor = (int[])edgeStart.Clone(); - for (int i = 0; i < connections.Count; i++) - { - (int from, int to, byte bit) = connections[i]; - int at = cursor[from]++; - edgeToGate[at] = to; - edgeToInputBit[at] = bit; - } - } - - private void CompileLuts() - { - int n = gateKinds.Count; - lutOffsets = new int[n]; - lutMasks = new int[n]; - - int total = 0; - for (int i = 0; i < n; i++) - { - if (gateKinds[i] != GateKind.Lut) - { - continue; - } - - int[]? table = lutTableByGate[i]; - if (table is null) - { - throw new InvalidOperationException("LUT gate is missing its truth table."); - } - - if (!IsPowerOfTwo(table.Length)) - { - throw new InvalidOperationException("LUT table length must be a power of two."); - } - - lutOffsets[i] = total; - lutMasks[i] = table.Length - 1; - total += table.Length; - } - - lutData = new int[total]; - int cursor = 0; - for (int i = 0; i < n; i++) - { - if (gateKinds[i] != GateKind.Lut) - { - continue; - } - - int[] table = lutTableByGate[i]!; - Array.Copy(table, 0, lutData, cursor, table.Length); - cursor += table.Length; - } - } - - private Action CompileComputeOutputsExpr() - { - ParameterExpression inputsParam = Expression.Parameter(typeof(int[]), "inputs"); - ParameterExpression outputsParam = Expression.Parameter(typeof(int[]), "outputs"); - ParameterExpression sourcesParam = Expression.Parameter(typeof(int[]), "sources"); - - ConstantExpression lutDataConst = Expression.Constant(lutData); - - Expression[] block = new Expression[gateKinds.Count]; - - for (int i = 0; i < gateKinds.Count; i++) - { - ConstantExpression idx = Expression.Constant(i); - Expression inMask = Expression.ArrayIndex(inputsParam, idx); - - Expression outExpr = gateKinds[i] switch - { - GateKind.Source => Expression.And(Expression.ArrayIndex(sourcesParam, idx), Expression.Constant(1)), - - GateKind.Not => Expression.Condition( - Expression.Equal(Expression.And(inMask, Expression.Constant(1)), Expression.Constant(0)), - Expression.Constant(1), - Expression.Constant(0)), - - GateKind.And2 => Expression.Condition( - Expression.Equal(Expression.And(inMask, Expression.Constant(0b11)), Expression.Constant(0b11)), - Expression.Constant(1), - Expression.Constant(0)), - - GateKind.Or2 => Expression.Condition( - Expression.NotEqual(Expression.And(inMask, Expression.Constant(0b11)), Expression.Constant(0)), - Expression.Constant(1), - Expression.Constant(0)), - - GateKind.Buffer => Expression.Condition( - Expression.NotEqual(Expression.And(inMask, Expression.Constant(1)), Expression.Constant(0)), - Expression.Constant(1), - Expression.Constant(0)), - - GateKind.Sink => Expression.Condition( - Expression.NotEqual(Expression.And(inMask, Expression.Constant(1)), Expression.Constant(0)), - Expression.Constant(1), - Expression.Constant(0)), - - GateKind.Lut => Expression.ArrayIndex( - lutDataConst, - Expression.Add( - Expression.Constant(lutOffsets[i]), - Expression.And(inMask, Expression.Constant(lutMasks[i])))), - - _ => throw new ArgumentOutOfRangeException(), - }; - - block[i] = Expression.Assign(Expression.ArrayAccess(outputsParam, idx), outExpr); - } - - BlockExpression body = Expression.Block(block); - return Expression.Lambda>(body, inputsParam, outputsParam, sourcesParam).Compile(); - } - - private static bool IsPowerOfTwo(int value) => value > 0 && (value & (value - 1)) == 0; - - private MacroLut? TryBuildMacroLut(CircuitDefinition definition, int maxSteps) - { - int inputCount = definition.InputPins.Count; - int outputCount = definition.OutputPins.Count; - - if (inputCount < 0 || outputCount <= 0) - { - return null; - } - - if (inputCount > 30) - { - throw new InvalidOperationException("Cannot build a LUT for macros with more than 30 inputs (would overflow 32-bit indexing)."); - } - - int patterns = 1 << inputCount; - - var sim = new ExprCircuitSimulator(); - foreach ((string macroName, MacroInfo macroInfo) in macroGates) - { - sim.macroGates[macroName] = macroInfo; - } - int[] map = CopyDefinitionGatesAndConnections(sim, definition, static (_, kind) => kind); - int[] inGates = MapPins(definition.InputPins, map); - int[] outGates = MapPins(definition.OutputPins, map); - - int[][] outputTables = new int[outputCount][]; - for (int o = 0; o < outputCount; o++) - { - outputTables[o] = new int[patterns]; - } - - for (int pattern = 0; pattern < patterns; pattern++) - { - sim.ClearSignals(); - - for (int i = 0; i < inputCount; i++) - { - bool bit = ((pattern >> i) & 1) != 0; - sim.SetSource(inGates[i], bit); - } - - if (!sim.TryRunUntilStable(maxSteps: maxSteps, out _)) - { - return null; - } - - for (int o = 0; o < outputCount; o++) - { - outputTables[o][pattern] = sim.GetOutput(outGates[o]) ? 1 : 0; - } - } - - return new MacroLut(inputCount, outputCount, outputTables); - } - - private MacroInstance AddMacroGateFromLut(string name, MacroLut lut) - { - int[] inputs = new int[lut.InputCount]; - for (int i = 0; i < inputs.Length; i++) - { - inputs[i] = AddGate(GateKind.Buffer); - } - - int[] outputs = new int[lut.OutputCount]; - for (int o = 0; o < outputs.Length; o++) - { - int lutGate = AddLutGate(lut.InputCount, lut.OutputTables[o]); - for (int i = 0; i < inputs.Length; i++) - { - ConnectGates(inputs[i], lutGate, toInputBit: i); - } - - int sink = AddGate(GateKind.Sink); - ConnectGates(lutGate, sink, toInputBit: 0); - outputs[o] = sink; - } - - return new MacroInstance(name, inputs, outputs); - } - - public IDisposable WatchGate(int gateId, Action callback) - { - if (callback is null) - { - throw new ArgumentNullException(nameof(callback)); - } - - if ((uint)gateId >= (uint)gateKinds.Count) - { - throw new ArgumentOutOfRangeException(nameof(gateId)); - } - - int id = nextWatcherId++; - var entry = new GateWatcherEntry(id, gateId, callback); - gateWatchers.Add(entry); - return new GateWatcherSubscription(this, id); - } - - private void RemoveWatcher(int id) - { - gateWatchers.RemoveAll(w => w.Id == id); - } - - private void NotifyWatchers() - { - if (gateWatchers.Count == 0) - { - return; - } - - GateWatcherEntry[] snapshot = gateWatchers.ToArray(); - for (int i = 0; i < snapshot.Length; i++) - { - GateWatcherEntry entry = snapshot[i]; - entry.Callback(entry.GateId, outputMasks[entry.GateId]); - } - } - - private sealed class GateWatcherSubscription : IDisposable - { - private readonly ExprCircuitSimulator simulator; - private readonly int id; - private bool disposed; - - public GateWatcherSubscription(ExprCircuitSimulator simulator, int id) - { - this.simulator = simulator; - this.id = id; - } - - public void Dispose() - { - if (disposed) - { - return; - } - - disposed = true; - simulator.RemoveWatcher(id); - } - } - - public bool TryRunUntilStable(int maxSteps, out int steps) - { - steps = 0; - if (maxSteps <= 0) - { - return false; - } - - EnsureCompiled(); - - while (steps < maxSteps) - { - steps++; - computeOutputs!(inputMasks, outputMasks, sourceStates); - bool changed = PropagateAndSwapDetectChange(); - NotifyWatchers(); - if (!changed) - { - return true; - } - } - - return false; - } - - private static int[] CopyDefinitionGatesAndConnections( - ExprCircuitSimulator destination, - CircuitDefinition definition, - Func mapKind) - { - int gateCount = definition.GateKinds.Count; - int[] map = new int[gateCount]; - Array.Fill(map, -1); - - // Expand nested macro instances by mapping their placeholder pin gates directly to the instantiated sub-macro pins. - for (int i = 0; i < definition.MacroInstances.Count; i++) - { - CircuitDefinition.MacroInstanceDef instanceDef = definition.MacroInstances[i]; - MacroInstance instance = destination.AddMacroGate(instanceDef.Name); - - if (instance.Inputs.Length != instanceDef.Inputs.Length || instance.Outputs.Length != instanceDef.Outputs.Length) - { - throw new InvalidOperationException($"Macro instance '{instanceDef.Name}' pin counts do not match the referenced macro definition."); - } - - for (int p = 0; p < instanceDef.Inputs.Length; p++) - { - int gateId = instanceDef.Inputs[p]; - if (map[gateId] != -1) throw new InvalidOperationException("Macro pin gate was mapped more than once."); - map[gateId] = instance.Inputs[p]; - } - - for (int p = 0; p < instanceDef.Outputs.Length; p++) - { - int gateId = instanceDef.Outputs[p]; - if (map[gateId] != -1) throw new InvalidOperationException("Macro pin gate was mapped more than once."); - map[gateId] = instance.Outputs[p]; - } - } - - for (int i = 0; i < gateCount; i++) - { - if (map[i] != -1) - { - continue; - } - - map[i] = destination.AddGate(mapKind(i, definition.GateKinds[i])); - } - - for (int i = 0; i < definition.Connections.Count; i++) - { - (int from, int to, byte bit) = definition.Connections[i]; - destination.ConnectGates(map[from], map[to], bit); - } - - return map; - } - - private static int[] MapPins(IReadOnlyList pins, int[] map) - { - int[] result = new int[pins.Count]; - for (int i = 0; i < result.Length; i++) - { - result[i] = map[pins[i]]; - } - - return result; - } -} diff --git a/StoneRed.LogicSimulator.Test/Program.cs b/StoneRed.LogicSimulator.Test/Program.cs deleted file mode 100644 index f4e0f5b..0000000 --- a/StoneRed.LogicSimulator.Test/Program.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace StoneRed.LogicSimulator.Test; - -internal static class Program -{ - public static void Main() - { - var sim = new ExprCircuitSimulator(); - - var inverter = new CircuitDefinition(); - int invIn = inverter.AddInputPin(); // Source inside definition - int invNot = inverter.AddGate(GateKind.Not); - int invOut = inverter.AddOutputPin(); // Sink inside definition - inverter.Connect(invIn, invNot, toInputBit: 0); - inverter.Connect(invNot, invOut, toInputBit: 0); - - sim.RegisterMacroGate("INV", inverter); - sim.ComputeLut("INV"); - - var doubleInverter = new CircuitDefinition(); - int inv2In = doubleInverter.AddInputPin(); - CircuitDefinition.MacroInstanceDef invA = doubleInverter.AddMacroInstance("INV", inputCount: 1, outputCount: 1); - CircuitDefinition.MacroInstanceDef invB = doubleInverter.AddMacroInstance("INV", inputCount: 1, outputCount: 1); - int inv2Out = doubleInverter.AddOutputPin(); - doubleInverter.Connect(inv2In, invA.Inputs[0], toInputBit: 0); - doubleInverter.Connect(invA.Outputs[0], invB.Inputs[0], toInputBit: 0); - doubleInverter.Connect(invB.Outputs[0], inv2Out, toInputBit: 0); - - sim.RegisterMacroGate("INV2", doubleInverter); - sim.ComputeLut("INV2"); - - int a = sim.AddGate(GateKind.Source); - ExprCircuitSimulator.MacroInstance inv2 = sim.AddMacroGate("INV2"); - int lamp = sim.AddGate(GateKind.Sink); - - sim.ConnectGates(a, inv2.Inputs[0], toInputBit: 0); - sim.ConnectGates(inv2.Outputs[0], lamp, toInputBit: 0); - - int httpSink = sim.AddGate(GateKind.Sink); - sim.ConnectGates(inv2.Outputs[0], httpSink, toInputBit: 0); - using var httpWatcher = sim.WatchGate(httpSink, (gateId, mask) => - { - if ((mask & 1) != 0) - { - Console.WriteLine($"HTTP gate {gateId} fired at mask={mask:X}"); - } - }); - - sim.SetSource(a, value: false); - sim.RunUntilStable(); - Console.WriteLine($"A=0 => Lamp={sim.GetOutput(lamp)} (expected False)"); - - sim.SetSource(a, value: true); - sim.RunUntilStable(); - Console.WriteLine($"A=1 => Lamp={sim.GetOutput(lamp)} (expected True)"); - } -} diff --git a/StoneRed.LogicSimulator.Tests/AdvancedCircuitTests.cs b/StoneRed.LogicSimulator.Tests/AdvancedCircuitTests.cs new file mode 100644 index 0000000..491de79 --- /dev/null +++ b/StoneRed.LogicSimulator.Tests/AdvancedCircuitTests.cs @@ -0,0 +1,201 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using StoneRed.LogicSimulator.Simulation; + +namespace StoneRed.LogicSimulator.Tests; + +[TestClass] +public class CycleAdvancedTests : AdvancedCircuitTestsBase +{ + protected override ICircuitSimulator CreateSimulator() + { + return new CycleCircuitSimulator(); + } +} + +[TestClass] +public class EventAdvancedTests : AdvancedCircuitTestsBase +{ + protected override ICircuitSimulator CreateSimulator() + { + return new EventCircuitSimulator(); + } +} + +public abstract class AdvancedCircuitTestsBase +{ + protected abstract ICircuitSimulator CreateSimulator(); + + [TestMethod] + public void TestSRLatch() + { + // SR Latch using NOR gates + // Q = NOR(R, Q') + // Q' = NOR(S, Q) + ICircuitSimulator sim = CreateSimulator(); + + int s = sim.AddGate(GateKind.Source); + int r = sim.AddGate(GateKind.Source); + + int orQ = sim.AddGate(GateKind.Or2); + int norQ = sim.AddGate(GateKind.Not); // Q + + int orQNot = sim.AddGate(GateKind.Or2); + int norQNot = sim.AddGate(GateKind.Not); // Q' + + // Q = NOR(R, Q') + sim.ConnectGates(r, orQ, 0); + sim.ConnectGates(norQNot, orQ, 1); + sim.ConnectGates(orQ, norQ, 0); + + // Q' = NOR(S, Q) + sim.ConnectGates(s, orQNot, 0); + sim.ConnectGates(norQ, orQNot, 1); + sim.ConnectGates(orQNot, norQNot, 0); + + int qSink = sim.AddGate(GateKind.Sink); + int qNotSink = sim.AddGate(GateKind.Sink); + sim.ConnectGates(norQ, qSink, 0); + sim.ConnectGates(norQNot, qNotSink, 0); + + // Reset state (R=1, S=0) -> Q=0, Q'=1 + sim.SetSource(r, true); + sim.SetSource(s, false); + _ = sim.RunUntilStable(10000); + Assert.IsFalse(sim.GetOutput(qSink), "Q should be 0 after Reset (R=1)"); + Assert.IsTrue(sim.GetOutput(qNotSink), "Q' should be 1 after Reset (R=1)"); + + // Hold (R=0, S=0) -> Q=0 + sim.SetSource(r, false); + _ = sim.RunUntilStable(10000); + Assert.IsFalse(sim.GetOutput(qSink), "Q should stay 0"); + + // Set state (R=0, S=1) -> Q=1, Q'=0 + sim.SetSource(s, true); + _ = sim.RunUntilStable(10000); + Assert.IsTrue(sim.GetOutput(qSink), "Q should be 1 after Set (S=1)"); + Assert.IsFalse(sim.GetOutput(qNotSink), "Q' should be 0 after Set (S=1)"); + + // Hold (R=0, S=0) -> Q=1 + sim.SetSource(s, false); + _ = sim.RunUntilStable(10000); + Assert.IsTrue(sim.GetOutput(qSink), "Q should stay 1"); + } + + [TestMethod] + public void TestFullAdder() + { + ICircuitSimulator sim = CreateSimulator(); + + int a = sim.AddGate(GateKind.Source); + int b = sim.AddGate(GateKind.Source); + int cin = sim.AddGate(GateKind.Source); + + int[] xorTable = { 0, 1, 1, 0 }; + int xor1 = sim.AddLutGate(2, xorTable); + int xor2 = sim.AddLutGate(2, xorTable); + + int and1 = sim.AddGate(GateKind.And2); + int and2 = sim.AddGate(GateKind.And2); + int or1 = sim.AddGate(GateKind.Or2); + + sim.ConnectGates(a, xor1, 0); + sim.ConnectGates(b, xor1, 1); + + sim.ConnectGates(xor1, xor2, 0); + sim.ConnectGates(cin, xor2, 1); + + sim.ConnectGates(a, and1, 0); + sim.ConnectGates(b, and1, 1); + + sim.ConnectGates(xor1, and2, 0); + sim.ConnectGates(cin, and2, 1); + + sim.ConnectGates(and1, or1, 0); + sim.ConnectGates(and2, or1, 1); + + int sumSink = sim.AddGate(GateKind.Sink); + int coutSink = sim.AddGate(GateKind.Sink); + sim.ConnectGates(xor2, sumSink, 0); + sim.ConnectGates(or1, coutSink, 0); + + void Check(bool iA, bool iB, bool iC, bool expectedSum, bool expectedCout) + { + sim.SetSource(a, iA); + sim.SetSource(b, iB); + sim.SetSource(cin, iC); + _ = sim.RunUntilStable(10000); + Assert.AreEqual(expectedSum, sim.GetOutput(sumSink), $"Sum failed for {iA},{iB},{iC}"); + Assert.AreEqual(expectedCout, sim.GetOutput(coutSink), $"Cout failed for {iA},{iB},{iC}"); + } + + Check(false, false, false, false, false); + Check(true, false, false, true, false); + Check(false, true, false, true, false); + Check(true, true, false, false, true); + Check(false, false, true, true, false); + Check(true, false, true, false, true); + Check(false, true, true, false, true); + Check(true, true, true, true, true); + } + + [TestMethod] + public void TestDLatch() + { + ICircuitSimulator sim = CreateSimulator(); + + int dSource = sim.AddGate(GateKind.Source); + int enSource = sim.AddGate(GateKind.Source); + + int notD = sim.AddGate(GateKind.Not); + sim.ConnectGates(dSource, notD, 0); + + int sAnd = sim.AddGate(GateKind.And2); + sim.ConnectGates(dSource, sAnd, 0); + sim.ConnectGates(enSource, sAnd, 1); + + int rAnd = sim.AddGate(GateKind.And2); + sim.ConnectGates(notD, rAnd, 0); + sim.ConnectGates(enSource, rAnd, 1); + + int orQ = sim.AddGate(GateKind.Or2); + int norQ = sim.AddGate(GateKind.Not); + + int orQNot = sim.AddGate(GateKind.Or2); + int norQNot = sim.AddGate(GateKind.Not); + + sim.ConnectGates(rAnd, orQ, 0); + sim.ConnectGates(norQNot, orQ, 1); + sim.ConnectGates(orQ, norQ, 0); + + sim.ConnectGates(sAnd, orQNot, 0); + sim.ConnectGates(norQ, orQNot, 1); + sim.ConnectGates(orQNot, norQNot, 0); + + int qSink = sim.AddGate(GateKind.Sink); + sim.ConnectGates(norQ, qSink, 0); + + // 1. Transparent mode (EN=1) -> Q follows D + sim.SetSource(enSource, true); + sim.SetSource(dSource, true); + _ = sim.RunUntilStable(10000); + Assert.IsTrue(sim.GetOutput(qSink), "Q should be 1 when D=1, EN=1"); + + sim.SetSource(dSource, false); + _ = sim.RunUntilStable(10000); + Assert.IsFalse(sim.GetOutput(qSink), "Q should be 0 when D=0, EN=1"); + + // 2. Latch mode (EN=0) -> Q stays same + sim.SetSource(dSource, true); + sim.SetSource(enSource, true); + _ = sim.RunUntilStable(10000); + Assert.IsTrue(sim.GetOutput(qSink)); + + sim.SetSource(enSource, false); // LATCH + _ = sim.RunUntilStable(10000); + + sim.SetSource(dSource, false); // Change D while latched + _ = sim.RunUntilStable(10000); + Assert.IsTrue(sim.GetOutput(qSink), "Q should stay 1 even if D changes while EN=0"); + } +} diff --git a/StoneRed.LogicSimulator.Tests/SimulatorTests.cs b/StoneRed.LogicSimulator.Tests/SimulatorTests.cs new file mode 100644 index 0000000..da3a452 --- /dev/null +++ b/StoneRed.LogicSimulator.Tests/SimulatorTests.cs @@ -0,0 +1,157 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using StoneRed.LogicSimulator.Simulation; + +namespace StoneRed.LogicSimulator.Tests; + +[TestClass] +public class CycleSimulatorTests : SimulatorTestsBase +{ + protected override ICircuitSimulator CreateSimulator() + { + return new CycleCircuitSimulator(); + } +} + +[TestClass] +public class EventSimulatorTests : SimulatorTestsBase +{ + protected override ICircuitSimulator CreateSimulator() + { + return new EventCircuitSimulator(); + } +} + +public abstract class SimulatorTestsBase +{ + protected abstract ICircuitSimulator CreateSimulator(); + + [TestMethod] + public void TestAndGate() + { + ICircuitSimulator sim = CreateSimulator(); + int s1 = sim.AddGate(GateKind.Source); + int s2 = sim.AddGate(GateKind.Source); + int and = sim.AddGate(GateKind.And2); + int sink = sim.AddGate(GateKind.Sink); + sim.ConnectGates(s1, and, 0); + sim.ConnectGates(s2, and, 1); + sim.ConnectGates(and, sink, 0); + + void Check(bool i1, bool i2, bool expected) + { + sim.SetSource(s1, i1); + sim.SetSource(s2, i2); + _ = sim.RunUntilStable(); + Assert.AreEqual(expected, sim.GetOutput(sink), $"AND2 failed for {i1} & {i2}"); + } + + sim.Reset(); + Check(false, false, false); + Check(false, true, false); + Check(true, false, false); + Check(true, true, true); + } + + [TestMethod] + public void TestOrGate() + { + ICircuitSimulator sim = CreateSimulator(); + int s1 = sim.AddGate(GateKind.Source); + int s2 = sim.AddGate(GateKind.Source); + int or = sim.AddGate(GateKind.Or2); + int sink = sim.AddGate(GateKind.Sink); + sim.ConnectGates(s1, or, 0); + sim.ConnectGates(s2, or, 1); + sim.ConnectGates(or, sink, 0); + + void Check(bool i1, bool i2, bool expected) + { + sim.SetSource(s1, i1); + sim.SetSource(s2, i2); + _ = sim.RunUntilStable(); + Assert.AreEqual(expected, sim.GetOutput(sink), $"OR2 failed for {i1} | {i2}"); + } + + sim.Reset(); + Check(false, false, false); + Check(false, true, true); + Check(true, false, true); + Check(true, true, true); + } + + [TestMethod] + public void TestNotGate() + { + ICircuitSimulator sim = CreateSimulator(); + int s1 = sim.AddGate(GateKind.Source); + int not = sim.AddGate(GateKind.Not); + int sink = sim.AddGate(GateKind.Sink); + sim.ConnectGates(s1, not, 0); + sim.ConnectGates(not, sink, 0); + + sim.Reset(); + sim.SetSource(s1, false); + _ = sim.RunUntilStable(); + Assert.IsTrue(sim.GetOutput(sink), "NOT(0) should be 1"); + + sim.SetSource(s1, true); + _ = sim.RunUntilStable(); + Assert.IsFalse(sim.GetOutput(sink), "NOT(1) should be 0"); + } + + [TestMethod] + public void TestLutGate() + { + ICircuitSimulator sim = CreateSimulator(); + int s1 = sim.AddGate(GateKind.Source); + int s2 = sim.AddGate(GateKind.Source); + int s3 = sim.AddGate(GateKind.Source); + + // Majority function (2 or more high) + int[] table = { 0, 0, 0, 1, 0, 1, 1, 1 }; + int lut = sim.AddLutGate(3, table); + int sink = sim.AddGate(GateKind.Sink); + + sim.ConnectGates(s1, lut, 0); + sim.ConnectGates(s2, lut, 1); + sim.ConnectGates(s3, lut, 2); + sim.ConnectGates(lut, sink, 0); + + sim.Reset(); + _ = sim.RunUntilStable(); + + sim.SetSource(s1, true); sim.SetSource(s2, true); sim.SetSource(s3, false); + _ = sim.RunUntilStable(); + Assert.IsTrue(sim.GetOutput(sink), "Majority(1,1,0) should be 1"); + + sim.SetSource(s1, false); sim.SetSource(s2, true); sim.SetSource(s3, false); + _ = sim.RunUntilStable(); + Assert.IsFalse(sim.GetOutput(sink), "Majority(0,1,0) should be 0"); + } + + [TestMethod] + public void TestMacroCorrectness() + { + ICircuitSimulator sim = CreateSimulator(); + CircuitDefinition def = new CircuitDefinition(); + int inPin = def.AddInputPin(); + int not = def.AddGate(GateKind.Not); + int outPin = def.AddOutputPin(); + def.Connect(inPin, not, 0); + def.Connect(not, outPin, 0); + + sim.RegisterMacroGate("NOT", def); + MacroInstance inst = sim.AddMacroGate("NOT"); + + int src = sim.AddGate(GateKind.Source); + int sink = sim.AddGate(GateKind.Sink); + sim.ConnectGates(src, inst.Inputs[0], 0); + sim.ConnectGates(inst.Outputs[0], sink, 0); + + sim.Reset(); + sim.SetSource(src, true); + _ = sim.RunUntilStable(); + Assert.IsFalse(sim.GetOutput(sink), "Macro NOT(1) should be 0"); + } +} diff --git a/StoneRed.LogicSimulator.Tests/StoneRed.LogicSimulator.Tests.csproj b/StoneRed.LogicSimulator.Tests/StoneRed.LogicSimulator.Tests.csproj new file mode 100644 index 0000000..6404eb7 --- /dev/null +++ b/StoneRed.LogicSimulator.Tests/StoneRed.LogicSimulator.Tests.csproj @@ -0,0 +1,18 @@ + + + net8.0 + enable + enable + false + + + + + + + + + + + + diff --git a/StoneRed.LogicSimulator.sln b/StoneRed.LogicSimulator.sln index 3253862..8d10c68 100644 --- a/StoneRed.LogicSimulator.sln +++ b/StoneRed.LogicSimulator.sln @@ -1,13 +1,17 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.4.11605.240 stable +VisualStudioVersion = 18.4.11605.240 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StoneRed.LogicSimulator", "StoneRed.LogicSimulator\StoneRed.LogicSimulator.csproj", "{7B259CCA-2947-4DF8-8252-7CF75FE39B8E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StoneRed.LogicSimulator.Api", "StoneRed.LogicSimulator.Api\StoneRed.LogicSimulator.Api.csproj", "{CD9C3C4D-4AF1-4A31-BDED-34CB91503B4C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StoneRed.LogicSimulator.Test", "StoneRed.LogicSimulator.Test\StoneRed.LogicSimulator.Test.csproj", "{996500F0-3CD5-912F-03D5-25011C2BD106}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StoneRed.LogicSimulator.Simulation", "StoneRed.LogicSimulator.Simulation\StoneRed.LogicSimulator.Simulation.csproj", "{08D6072E-CEC7-FA4B-00E7-C0C88E8C5A38}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StoneRed.LogicSimulator.Benchmarks", "StoneRed.LogicSimulator.Benchmarks\StoneRed.LogicSimulator.Benchmarks.csproj", "{7F65C796-E597-2D31-BA0C-EE9A672A68EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StoneRed.LogicSimulator.Tests", "StoneRed.LogicSimulator.Tests\StoneRed.LogicSimulator.Tests.csproj", "{82A09AEE-FA5E-7E76-29A2-4FB1B78E3CD5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -23,10 +27,18 @@ Global {CD9C3C4D-4AF1-4A31-BDED-34CB91503B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD9C3C4D-4AF1-4A31-BDED-34CB91503B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD9C3C4D-4AF1-4A31-BDED-34CB91503B4C}.Release|Any CPU.Build.0 = Release|Any CPU - {996500F0-3CD5-912F-03D5-25011C2BD106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {996500F0-3CD5-912F-03D5-25011C2BD106}.Debug|Any CPU.Build.0 = Debug|Any CPU - {996500F0-3CD5-912F-03D5-25011C2BD106}.Release|Any CPU.ActiveCfg = Release|Any CPU - {996500F0-3CD5-912F-03D5-25011C2BD106}.Release|Any CPU.Build.0 = Release|Any CPU + {08D6072E-CEC7-FA4B-00E7-C0C88E8C5A38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08D6072E-CEC7-FA4B-00E7-C0C88E8C5A38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08D6072E-CEC7-FA4B-00E7-C0C88E8C5A38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08D6072E-CEC7-FA4B-00E7-C0C88E8C5A38}.Release|Any CPU.Build.0 = Release|Any CPU + {7F65C796-E597-2D31-BA0C-EE9A672A68EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F65C796-E597-2D31-BA0C-EE9A672A68EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F65C796-E597-2D31-BA0C-EE9A672A68EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F65C796-E597-2D31-BA0C-EE9A672A68EB}.Release|Any CPU.Build.0 = Release|Any CPU + {82A09AEE-FA5E-7E76-29A2-4FB1B78E3CD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82A09AEE-FA5E-7E76-29A2-4FB1B78E3CD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82A09AEE-FA5E-7E76-29A2-4FB1B78E3CD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82A09AEE-FA5E-7E76-29A2-4FB1B78E3CD5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE