From c40195d0a32009b74f2276998e1c7e71119a35fb Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 26 Mar 2026 01:19:25 +0100 Subject: [PATCH] Notify watchers only on output changes and pass previous values to callbacks --- .../CycleCircuitSimulator.cs | 17 +++++-- .../SimulatorBase.cs | 19 +++++--- .../SimulatorTests.cs | 47 +++++++++++++++++-- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs b/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs index 68b58f5..208413b 100644 --- a/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs +++ b/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs @@ -6,6 +6,7 @@ public sealed class CycleCircuitSimulator : SimulatorBase { private int[] nextInputMasks = []; private Action computeOutputs = (_, _, _) => { }; + private int[] previousOutputMasks = []; protected override void EnsureStorage() { @@ -13,6 +14,7 @@ public sealed class CycleCircuitSimulator : SimulatorBase if (nextInputMasks.Length != gateKinds.Count) { nextInputMasks = new int[gateKinds.Count]; + previousOutputMasks = new int[gateKinds.Count]; } } @@ -20,10 +22,9 @@ public sealed class CycleCircuitSimulator : SimulatorBase { base.Reset(); Array.Clear(nextInputMasks); + Array.Clear(previousOutputMasks); } - protected override void OnSourceChanged(int gateId) { } - public override void Step() { EnsureCompiled(); @@ -32,9 +33,13 @@ public sealed class CycleCircuitSimulator : SimulatorBase Reset(); } + (outputMasks, previousOutputMasks) = (previousOutputMasks, outputMasks); computeOutputs(inputMasks, outputMasks, sourceStates); PropagateAndSwap(); - NotifyAllWatchers(); + if (hasAnyWatchers) + { + NotifyAllWatchers(previousOutputMasks); + } } private void PropagateAndSwap() @@ -103,9 +108,13 @@ public sealed class CycleCircuitSimulator : SimulatorBase while (changed && steps < maxSteps) { steps++; + (outputMasks, previousOutputMasks) = (previousOutputMasks, outputMasks); computeOutputs(inputMasks, outputMasks, sourceStates); changed = PropagateAndSwapDetectChange(); - NotifyAllWatchers(); + if (hasAnyWatchers) + { + NotifyAllWatchers(previousOutputMasks); + } } return !changed; } diff --git a/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs b/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs index 2fcf782..56c9a6e 100644 --- a/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs +++ b/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs @@ -28,6 +28,7 @@ public abstract class SimulatorBase : ICircuitSimulator private readonly List allWatchers = []; private Action[][] watcherCache = []; private int[] gatesWithWatchers = []; + protected bool hasAnyWatchers; protected int nextWatcherId; protected sealed record GateWatcherEntry(int Id, int GateId, Action Callback); @@ -169,6 +170,7 @@ public abstract class SimulatorBase : ICircuitSimulator GateWatcherEntry entry = new GateWatcherEntry(id, gateId, callback); allWatchers.Add(entry); RebuildWatcherCache(); + compiled = false; return new GateWatcherSubscription(this, id); } @@ -176,6 +178,7 @@ public abstract class SimulatorBase : ICircuitSimulator { _ = allWatchers.RemoveAll(w => w.Id == id); RebuildWatcherCache(); + compiled = false; } private void RebuildWatcherCache() @@ -193,18 +196,22 @@ public abstract class SimulatorBase : ICircuitSimulator } gatesWithWatchers = [.. activeGates]; + hasAnyWatchers = allWatchers.Count > 0; } - protected void NotifyAllWatchers() + protected void NotifyAllWatchers(int[] previousOutputMasks) { 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++) + if (outputMasks[gateId] != previousOutputMasks[gateId]) { - callbacks[j](gateId, val); + Action[] callbacks = watcherCache[gateId]; + int val = outputMasks[gateId]; + for (int j = 0; j < callbacks.Length; j++) + { + callbacks[j](gateId, val); + } } } } @@ -275,7 +282,7 @@ public abstract class SimulatorBase : ICircuitSimulator 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(), + _ => throw new InvalidOperationException($"Unknown gate kind: {gateKinds[gateId]}") }; } diff --git a/StoneRed.LogicSimulator.Tests/SimulatorTests.cs b/StoneRed.LogicSimulator.Tests/SimulatorTests.cs index da3a452..1d9f86d 100644 --- a/StoneRed.LogicSimulator.Tests/SimulatorTests.cs +++ b/StoneRed.LogicSimulator.Tests/SimulatorTests.cs @@ -46,7 +46,6 @@ public abstract class SimulatorTestsBase 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); @@ -73,7 +72,6 @@ public abstract class SimulatorTestsBase 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); @@ -90,7 +88,6 @@ public abstract class SimulatorTestsBase 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"); @@ -118,7 +115,6 @@ public abstract class SimulatorTestsBase 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); @@ -149,9 +145,50 @@ public abstract class SimulatorTestsBase 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"); } + + [TestMethod] + public void TestWatcherNotification() + { + ICircuitSimulator sim = CreateSimulator(); + int source = sim.AddGate(GateKind.Source); + int not = sim.AddGate(GateKind.Not); + sim.ConnectGates(source, not, 0); + + int watcher1Count = 0; + int watcher2Count = 0; + + IDisposable sub1 = sim.WatchGate(not, (id, val) => watcher1Count++); + IDisposable sub2 = sim.WatchGate(not, (id, val) => watcher2Count++); + + // Initial state + sim.SetSource(source, false); + _ = sim.RunUntilStable(); + Assert.AreEqual(1, watcher1Count, "Watcher 1 should fire on initial change"); + Assert.AreEqual(1, watcher2Count, "Watcher 2 should fire on initial change"); + + // No change + sim.SetSource(source, false); + _ = sim.RunUntilStable(); + Assert.AreEqual(1, watcher1Count, "Watcher 1 should not fire if no change"); + Assert.AreEqual(1, watcher2Count, "Watcher 2 should not fire if no change"); + + // Change + sim.SetSource(source, true); + _ = sim.RunUntilStable(); + Assert.AreEqual(2, watcher1Count, "Watcher 1 should fire on second change"); + Assert.AreEqual(2, watcher2Count, "Watcher 2 should fire on second change"); + + // Dispose one watcher + sub2.Dispose(); + + // Change again + sim.SetSource(source, false); + _ = sim.RunUntilStable(); + Assert.AreEqual(3, watcher1Count, "Watcher 1 should fire after sub2 is disposed"); + Assert.AreEqual(2, watcher2Count, "Watcher 2 should NOT fire after being disposed"); + } }