Notify watchers only on output changes and pass previous values to callbacks

This commit is contained in:
Stone_Red
2026-03-26 01:19:25 +01:00
parent aa6d13a46c
commit c40195d0a3
3 changed files with 68 additions and 15 deletions
@@ -6,6 +6,7 @@ public sealed class CycleCircuitSimulator : SimulatorBase
{ {
private int[] nextInputMasks = []; private int[] nextInputMasks = [];
private Action<int[], int[], int[]> computeOutputs = (_, _, _) => { }; private Action<int[], int[], int[]> computeOutputs = (_, _, _) => { };
private int[] previousOutputMasks = [];
protected override void EnsureStorage() protected override void EnsureStorage()
{ {
@@ -13,6 +14,7 @@ public sealed class CycleCircuitSimulator : SimulatorBase
if (nextInputMasks.Length != gateKinds.Count) if (nextInputMasks.Length != gateKinds.Count)
{ {
nextInputMasks = new int[gateKinds.Count]; nextInputMasks = new int[gateKinds.Count];
previousOutputMasks = new int[gateKinds.Count];
} }
} }
@@ -20,10 +22,9 @@ public sealed class CycleCircuitSimulator : SimulatorBase
{ {
base.Reset(); base.Reset();
Array.Clear(nextInputMasks); Array.Clear(nextInputMasks);
Array.Clear(previousOutputMasks);
} }
protected override void OnSourceChanged(int gateId) { }
public override void Step() public override void Step()
{ {
EnsureCompiled(); EnsureCompiled();
@@ -32,9 +33,13 @@ public sealed class CycleCircuitSimulator : SimulatorBase
Reset(); Reset();
} }
(outputMasks, previousOutputMasks) = (previousOutputMasks, outputMasks);
computeOutputs(inputMasks, outputMasks, sourceStates); computeOutputs(inputMasks, outputMasks, sourceStates);
PropagateAndSwap(); PropagateAndSwap();
NotifyAllWatchers(); if (hasAnyWatchers)
{
NotifyAllWatchers(previousOutputMasks);
}
} }
private void PropagateAndSwap() private void PropagateAndSwap()
@@ -103,9 +108,13 @@ public sealed class CycleCircuitSimulator : SimulatorBase
while (changed && steps < maxSteps) while (changed && steps < maxSteps)
{ {
steps++; steps++;
(outputMasks, previousOutputMasks) = (previousOutputMasks, outputMasks);
computeOutputs(inputMasks, outputMasks, sourceStates); computeOutputs(inputMasks, outputMasks, sourceStates);
changed = PropagateAndSwapDetectChange(); changed = PropagateAndSwapDetectChange();
NotifyAllWatchers(); if (hasAnyWatchers)
{
NotifyAllWatchers(previousOutputMasks);
}
} }
return !changed; return !changed;
} }
@@ -28,6 +28,7 @@ public abstract class SimulatorBase : ICircuitSimulator
private readonly List<GateWatcherEntry> allWatchers = []; private readonly List<GateWatcherEntry> allWatchers = [];
private Action<int, int>[][] watcherCache = []; private Action<int, int>[][] watcherCache = [];
private int[] gatesWithWatchers = []; private int[] gatesWithWatchers = [];
protected bool hasAnyWatchers;
protected int nextWatcherId; protected int nextWatcherId;
protected sealed record GateWatcherEntry(int Id, int GateId, Action<int, int> Callback); protected sealed record GateWatcherEntry(int Id, int GateId, Action<int, int> Callback);
@@ -169,6 +170,7 @@ public abstract class SimulatorBase : ICircuitSimulator
GateWatcherEntry entry = new GateWatcherEntry(id, gateId, callback); GateWatcherEntry entry = new GateWatcherEntry(id, gateId, callback);
allWatchers.Add(entry); allWatchers.Add(entry);
RebuildWatcherCache(); RebuildWatcherCache();
compiled = false;
return new GateWatcherSubscription(this, id); return new GateWatcherSubscription(this, id);
} }
@@ -176,6 +178,7 @@ public abstract class SimulatorBase : ICircuitSimulator
{ {
_ = allWatchers.RemoveAll(w => w.Id == id); _ = allWatchers.RemoveAll(w => w.Id == id);
RebuildWatcherCache(); RebuildWatcherCache();
compiled = false;
} }
private void RebuildWatcherCache() private void RebuildWatcherCache()
@@ -193,18 +196,22 @@ public abstract class SimulatorBase : ICircuitSimulator
} }
gatesWithWatchers = [.. activeGates]; gatesWithWatchers = [.. activeGates];
hasAnyWatchers = allWatchers.Count > 0;
} }
protected void NotifyAllWatchers() protected void NotifyAllWatchers(int[] previousOutputMasks)
{ {
for (int i = 0; i < gatesWithWatchers.Length; i++) for (int i = 0; i < gatesWithWatchers.Length; i++)
{ {
int gateId = gatesWithWatchers[i]; int gateId = gatesWithWatchers[i];
Action<int, int>[] callbacks = watcherCache[gateId]; if (outputMasks[gateId] != previousOutputMasks[gateId])
int val = outputMasks[gateId];
for (int j = 0; j < callbacks.Length; j++)
{ {
callbacks[j](gateId, val); Action<int, int>[] 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.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.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])))), 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]}")
}; };
} }
@@ -46,7 +46,6 @@ public abstract class SimulatorTestsBase
Assert.AreEqual(expected, sim.GetOutput(sink), $"AND2 failed for {i1} & {i2}"); Assert.AreEqual(expected, sim.GetOutput(sink), $"AND2 failed for {i1} & {i2}");
} }
sim.Reset();
Check(false, false, false); Check(false, false, false);
Check(false, true, false); Check(false, true, false);
Check(true, false, false); Check(true, false, false);
@@ -73,7 +72,6 @@ public abstract class SimulatorTestsBase
Assert.AreEqual(expected, sim.GetOutput(sink), $"OR2 failed for {i1} | {i2}"); Assert.AreEqual(expected, sim.GetOutput(sink), $"OR2 failed for {i1} | {i2}");
} }
sim.Reset();
Check(false, false, false); Check(false, false, false);
Check(false, true, true); Check(false, true, true);
Check(true, false, true); Check(true, false, true);
@@ -90,7 +88,6 @@ public abstract class SimulatorTestsBase
sim.ConnectGates(s1, not, 0); sim.ConnectGates(s1, not, 0);
sim.ConnectGates(not, sink, 0); sim.ConnectGates(not, sink, 0);
sim.Reset();
sim.SetSource(s1, false); sim.SetSource(s1, false);
_ = sim.RunUntilStable(); _ = sim.RunUntilStable();
Assert.IsTrue(sim.GetOutput(sink), "NOT(0) should be 1"); 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(s3, lut, 2);
sim.ConnectGates(lut, sink, 0); sim.ConnectGates(lut, sink, 0);
sim.Reset();
_ = sim.RunUntilStable(); _ = sim.RunUntilStable();
sim.SetSource(s1, true); sim.SetSource(s2, true); sim.SetSource(s3, false); 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(src, inst.Inputs[0], 0);
sim.ConnectGates(inst.Outputs[0], sink, 0); sim.ConnectGates(inst.Outputs[0], sink, 0);
sim.Reset();
sim.SetSource(src, true); sim.SetSource(src, true);
_ = sim.RunUntilStable(); _ = sim.RunUntilStable();
Assert.IsFalse(sim.GetOutput(sink), "Macro NOT(1) should be 0"); 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");
}
} }