From 1deb90a1f0ae56eaa7e054a815b729dea2f73d64 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Fri, 27 Mar 2026 03:09:52 +0100 Subject: [PATCH] Switch gate watchers to notify on input mask changes, add DivideByNCounter --- .../CycleCircuitSimulator.cs | 15 ++- .../EventCircuitSimulator.cs | 5 +- .../ICircuitSimulator.cs | 6 +- .../SimulatorBase.cs | 14 +- .../SimulatorTests.cs | 16 +-- .../Simulation/LogicGates/DivideByNCounter.cs | 126 ++++++++++++++++++ 6 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 StoneRed.LogicSimulator/Simulation/LogicGates/DivideByNCounter.cs diff --git a/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs b/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs index 89f4350..8e16f9f 100644 --- a/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs +++ b/StoneRed.LogicSimulator.Simulation/CycleCircuitSimulator.cs @@ -17,6 +17,7 @@ public sealed class CycleCircuitSimulator : SimulatorBase private int[] nextInputMasks = []; private Action computeOutputs = (_, _, _) => { }; private int[] previousOutputMasks = []; + private int[] previousInputMasks = []; /// /// Ensures internal storage arrays are properly sized for the current gate count. @@ -28,6 +29,7 @@ public sealed class CycleCircuitSimulator : SimulatorBase { nextInputMasks = new int[gateKinds.Count]; previousOutputMasks = new int[gateKinds.Count]; + previousInputMasks = new int[gateKinds.Count]; } } @@ -39,6 +41,7 @@ public sealed class CycleCircuitSimulator : SimulatorBase base.Reset(); Array.Clear(nextInputMasks); Array.Clear(previousOutputMasks); + Array.Clear(previousInputMasks); } /// @@ -55,10 +58,14 @@ public sealed class CycleCircuitSimulator : SimulatorBase (outputMasks, previousOutputMasks) = (previousOutputMasks, outputMasks); computeOutputs(inputMasks, outputMasks, sourceStates); + if (hasAnyWatchers) + { + Array.Copy(inputMasks, previousInputMasks, inputMasks.Length); + } PropagateAndSwap(); if (hasAnyWatchers) { - NotifyAllWatchers(previousOutputMasks); + NotifyAllWatchers(previousInputMasks); } } @@ -136,10 +143,14 @@ public sealed class CycleCircuitSimulator : SimulatorBase steps++; (outputMasks, previousOutputMasks) = (previousOutputMasks, outputMasks); computeOutputs(inputMasks, outputMasks, sourceStates); + if (hasAnyWatchers) + { + Array.Copy(inputMasks, previousInputMasks, inputMasks.Length); + } changed = PropagateAndSwapDetectChange(); if (hasAnyWatchers) { - NotifyAllWatchers(previousOutputMasks); + NotifyAllWatchers(previousInputMasks); } } return !changed; diff --git a/StoneRed.LogicSimulator.Simulation/EventCircuitSimulator.cs b/StoneRed.LogicSimulator.Simulation/EventCircuitSimulator.cs index 1172c52..2a740eb 100644 --- a/StoneRed.LogicSimulator.Simulation/EventCircuitSimulator.cs +++ b/StoneRed.LogicSimulator.Simulation/EventCircuitSimulator.cs @@ -84,7 +84,6 @@ public sealed class EventCircuitSimulator : SimulatorBase if (outputMasks[gateId] != oldOutput) { Propagate(gateId); - NotifyGateWatchers(gateId); } } } @@ -103,6 +102,10 @@ public sealed class EventCircuitSimulator : SimulatorBase if (oldBit != outVal) { inputMasks[toGate] ^= 1 << bit; + if (hasAnyWatchers) + { + NotifyGateWatchers(toGate); + } Enqueue(toGate); } } diff --git a/StoneRed.LogicSimulator.Simulation/ICircuitSimulator.cs b/StoneRed.LogicSimulator.Simulation/ICircuitSimulator.cs index 4cd9521..76abf2b 100644 --- a/StoneRed.LogicSimulator.Simulation/ICircuitSimulator.cs +++ b/StoneRed.LogicSimulator.Simulation/ICircuitSimulator.cs @@ -114,11 +114,11 @@ public interface ICircuitSimulator bool TryRunUntilStable(int maxSteps, out int steps); /// - /// Subscribes to output changes on a specific gate. - /// The callback is invoked whenever the gate's output value changes. + /// Subscribes to input mask changes on a specific gate. + /// The callback is invoked whenever the gate's input mask changes. /// /// The gate ID to watch. - /// Action to invoke on change. Parameters are (gateId, newOutputMask). + /// Action to invoke on change. Parameters are (gateId, newInputMask). /// An that unsubscribes the watcher when disposed. /// Thrown when callback is null. /// Thrown when gateId is invalid. diff --git a/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs b/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs index 5a45d46..66706da 100644 --- a/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs +++ b/StoneRed.LogicSimulator.Simulation/SimulatorBase.cs @@ -246,18 +246,18 @@ public abstract class SimulatorBase : ICircuitSimulator } /// - /// Notifies all registered watchers of gates that changed between the previous and current output states. + /// Notifies all registered watchers of gates that changed between the previous and current input states. /// - /// The output states from before the change. - protected void NotifyAllWatchers(int[] previousOutputMasks) + /// The input states from before the change. + protected void NotifyAllWatchers(int[] previousInputMasks) { for (int i = 0; i < gatesWithWatchers.Length; i++) { int gateId = gatesWithWatchers[i]; - if (outputMasks[gateId] != previousOutputMasks[gateId]) + if (inputMasks[gateId] != previousInputMasks[gateId]) { Action[] callbacks = watcherCache[gateId]; - int val = outputMasks[gateId]; + int val = inputMasks[gateId]; for (int j = 0; j < callbacks.Length; j++) { callbacks[j](gateId, val); @@ -267,7 +267,7 @@ public abstract class SimulatorBase : ICircuitSimulator } /// - /// Notifies watchers of a specific gate that its output has changed. + /// Notifies watchers of a specific gate that its input has changed. /// /// The ID of the gate that changed. protected void NotifyGateWatchers(int gateId) @@ -283,7 +283,7 @@ public abstract class SimulatorBase : ICircuitSimulator return; } - int val = outputMasks[gateId]; + int val = inputMasks[gateId]; for (int i = 0; i < callbacks.Length; i++) { callbacks[i](gateId, val); diff --git a/StoneRed.LogicSimulator.Tests/SimulatorTests.cs b/StoneRed.LogicSimulator.Tests/SimulatorTests.cs index 1d9f86d..4d688e8 100644 --- a/StoneRed.LogicSimulator.Tests/SimulatorTests.cs +++ b/StoneRed.LogicSimulator.Tests/SimulatorTests.cs @@ -167,20 +167,20 @@ public abstract class SimulatorTestsBase // 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"); + Assert.AreEqual(0, watcher1Count, "Watcher 1 should not fire when input mask does not change"); + Assert.AreEqual(0, watcher2Count, "Watcher 2 should not fire when input mask does not 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"); + Assert.AreEqual(0, watcher1Count, "Watcher 1 should not fire if no change"); + Assert.AreEqual(0, 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"); + Assert.AreEqual(1, watcher1Count, "Watcher 1 should fire on input change"); + Assert.AreEqual(1, watcher2Count, "Watcher 2 should fire on input change"); // Dispose one watcher sub2.Dispose(); @@ -188,7 +188,7 @@ public abstract class SimulatorTestsBase // 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"); + Assert.AreEqual(2, watcher1Count, "Watcher 1 should fire after sub2 is disposed"); + Assert.AreEqual(1, watcher2Count, "Watcher 2 should NOT fire after being disposed"); } } diff --git a/StoneRed.LogicSimulator/Simulation/LogicGates/DivideByNCounter.cs b/StoneRed.LogicSimulator/Simulation/LogicGates/DivideByNCounter.cs new file mode 100644 index 0000000..4f721ca --- /dev/null +++ b/StoneRed.LogicSimulator/Simulation/LogicGates/DivideByNCounter.cs @@ -0,0 +1,126 @@ +using MonoGame.Extended.Input; + +using StoneRed.LogicSimulator.Api; +using StoneRed.LogicSimulator.Api.Attributes; +using StoneRed.LogicSimulator.Api.Interfaces; + +using System; + +namespace StoneRed.LogicSimulator.Simulation.LogicGates; + +[LogicGateName("DivideByNCounter")] +[LogicGateDescription("A clock is a circuit that oscillates between a high and a low state.")] +internal class DivideByNCounter : LogicGate, IInteractable +{ + private ICircuitSimulator? circuitSimulator; + private int gateId; + private int count = 0; + private int tickRate = 0; + private bool currentState = false; + private bool inputInitialized = false; + private bool lastInputState = false; + + public override int OutputCount { get; set; } = 1; + + public override int InputCount { get; set; } = 1; + + public string Info + { + get + { + if (tickRate <= 0) + { + return "Disabled"; + } + + return tickRate + "\n" + ((count > tickRate) ? count - tickRate : count); + } + } + + public void OnInteraction(MouseStateExtended mouseState, MouseStateExtended previousMouseState, KeyboardStateExtended keyboardStateExtended) + { + if (mouseState.DeltaScrollWheelValue == 0 || !keyboardStateExtended.IsShiftDown()) + { + return; + } + + if (mouseState.DeltaScrollWheelValue < 0 && tickRate < int.MaxValue - 10) + { + tickRate += keyboardStateExtended.IsControlDown() ? 10 : 1; + } + else if (tickRate >= 1) + { + tickRate -= keyboardStateExtended.IsControlDown() ? 10 : 1; + } + + tickRate = Math.Clamp(tickRate, 0, int.MaxValue); + + if (tickRate <= 0) + { + count = 0; + currentState = false; + circuitSimulator?.SetSource(gateId, false); + } + } + + protected internal override void Register(ICircuitSimulator circuitSimulator) + { + this.circuitSimulator = circuitSimulator; + SimulatorGateId = circuitSimulator.AddGate(GateKind.Source); + gateId = SimulatorGateId; + count = 0; + currentState = false; + inputInitialized = false; + lastInputState = false; + circuitSimulator.SetSource(gateId, false); + _ = circuitSimulator.WatchGate(SimulatorGateId, (_, newInputMask) => OnInputChanged((newInputMask & 1) != 0)); + } + + private void OnInputChanged(bool inputState) + { + if (circuitSimulator == null) + { + return; + } + + if (!inputInitialized) + { + inputInitialized = true; + lastInputState = inputState; + return; + } + + if (lastInputState == inputState) + { + return; + } + + lastInputState = inputState; + + if (tickRate <= 0) + { + if (count != 0 || currentState) + { + count = 0; + currentState = false; + circuitSimulator.SetSource(gateId, false); + } + + return; + } + + count++; + + if (count >= tickRate * 2) + { + count = 0; + } + + bool newState = count > tickRate; + if (newState != currentState) + { + currentState = newState; + circuitSimulator.SetSource(gateId, newState); + } + } +}