diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d07e46a..df679e7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,33 +15,33 @@ jobs: with: path: ${{ github.event.repository.name }} - - name: Checkout `worlds` + - name: Checkout `types` uses: actions/checkout@v4.1.2 with: - repository: simulation-tree/worlds + repository: simulation-tree/types token: ${{ secrets.PAT }} - path: worlds + path: types - - name: Checkout `types` + - name: Checkout `collections` uses: actions/checkout@v4.1.2 with: - repository: simulation-tree/types + repository: simulation-tree/collections token: ${{ secrets.PAT }} - path: types + path: collections - - name: Checkout `unmanaged` + - name: Checkout `worlds` uses: actions/checkout@v4.1.2 with: - repository: simulation-tree/unmanaged + repository: simulation-tree/worlds token: ${{ secrets.PAT }} - path: unmanaged + path: worlds - - name: Checkout `collections` + - name: Checkout `unmanaged` uses: actions/checkout@v4.1.2 with: - repository: simulation-tree/collections + repository: simulation-tree/unmanaged token: ${{ secrets.PAT }} - path: collections + path: unmanaged - name: Setup uses: actions/setup-dotnet@v4 @@ -51,15 +51,27 @@ jobs: - name: Set VERSION variable from tag run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV + - name: Build `Simulation.Core` + run: dotnet build "${{ github.event.repository.name }}/core" -c Debug /p:Version=${VERSION} + - name: Build `Simulation.Core` run: dotnet build "${{ github.event.repository.name }}/core" -c Release /p:Version=${VERSION} + - name: Build `Simulation.Generator` + run: dotnet build "${{ github.event.repository.name }}/generator" -c Debug /p:Version=${VERSION} + - name: Build `Simulation.Generator` run: dotnet build "${{ github.event.repository.name }}/generator" -c Release /p:Version=${VERSION} + - name: Build `Simulation` + run: dotnet build "${{ github.event.repository.name }}/source" -c Debug /p:Version=${VERSION} + - name: Build `Simulation` run: dotnet build "${{ github.event.repository.name }}/source" -c Release /p:Version=${VERSION} + - name: Build `Simulation.Tests` + run: dotnet build "${{ github.event.repository.name }}/tests" -c Debug /p:Version=${VERSION} + - name: Build `Simulation.Tests` run: dotnet build "${{ github.event.repository.name }}/tests" -c Release /p:Version=${VERSION} @@ -67,13 +79,13 @@ jobs: run: dotnet test "${{ github.event.repository.name }}/tests" -c Release --logger "trx" - name: Pack `Simulation.Core` - run: dotnet pack "${{ github.event.repository.name }}/core" -c Release /p:Version=${VERSION} --no-build --output . + run: dotnet pack "${{ github.event.repository.name }}/core" /p:Version=${VERSION} --no-build --output . - name: Pack `Simulation.Generator` - run: dotnet pack "${{ github.event.repository.name }}/generator" -c Release /p:Version=${VERSION} --no-build --output . + run: dotnet pack "${{ github.event.repository.name }}/generator" /p:Version=${VERSION} --no-build --output . - name: Pack `Simulation` - run: dotnet pack "${{ github.event.repository.name }}/source" -c Release /p:Version=${VERSION} --no-build --output . + run: dotnet pack "${{ github.event.repository.name }}/source" /p:Version=${VERSION} --no-build --output . - name: Add NuGet Source run: dotnet nuget add source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json --name github --username ${{ github.repository_owner }} --password ${{ github.token }} --store-password-in-clear-text diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be0882a..9a53745 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,33 +28,33 @@ jobs: with: path: ${{ github.event.repository.name }} - - name: Checkout `worlds` + - name: Checkout `types` uses: actions/checkout@v4.1.2 with: - repository: simulation-tree/worlds + repository: simulation-tree/types token: ${{ secrets.PAT }} - path: worlds + path: types - - name: Checkout `types` + - name: Checkout `collections` uses: actions/checkout@v4.1.2 with: - repository: simulation-tree/types + repository: simulation-tree/collections token: ${{ secrets.PAT }} - path: types + path: collections - - name: Checkout `unmanaged` + - name: Checkout `worlds` uses: actions/checkout@v4.1.2 with: - repository: simulation-tree/unmanaged + repository: simulation-tree/worlds token: ${{ secrets.PAT }} - path: unmanaged + path: worlds - - name: Checkout `collections` + - name: Checkout `unmanaged` uses: actions/checkout@v4.1.2 with: - repository: simulation-tree/collections + repository: simulation-tree/unmanaged token: ${{ secrets.PAT }} - path: collections + path: unmanaged - name: Setup uses: actions/setup-dotnet@v4 diff --git a/core/Simulation.Core.csproj b/core/Simulation.Core.csproj index ce2db7f..b375133 100644 --- a/core/Simulation.Core.csproj +++ b/core/Simulation.Core.csproj @@ -1,35 +1,41 @@ - + - - net9.0 - disable - enable - True - True - True - True - popcron - simulation-tree - Logic upon data - https://github.com/simulation-tree/simulation - README.md - ecs - Simulation Core - Simulation - - True - + + net9.0 + disable + enable + True + True + True + True + popcron + simulation-tree + Logic upon data + https://github.com/simulation-tree/simulation + README.md + ecs + Simulation Core + Simulation + True + false + bin/$(TargetFramework)/$(Configuration) + - - - True - \ - - + + + True + \ + + - - - - + + + + + + + + + \ No newline at end of file diff --git a/core/SystemGroup.cs b/core/SystemGroup.cs new file mode 100644 index 0000000..0066410 --- /dev/null +++ b/core/SystemGroup.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace Simulation +{ + /// + /// A group of systems that all run on a separate foreground thread. + /// + public sealed class SystemGroup : IDisposable, IListener where T : unmanaged + { + private readonly CancellationTokenSource cts; + private readonly Thread thread; + private IListener[] systems; + private bool disposed; + private volatile int messageVersion; + private volatile bool finishedSignal; + private T message; + + int IListener.Count => 1; + + /// + /// Creates a new system group. + /// + public SystemGroup(ReadOnlySpan threadName) + { + systems = []; + cts = new(); + thread = new Thread(Run); + thread.Name = threadName.ToString(); + thread.IsBackground = false; + thread.Start(); + } + + void IListener.CollectMessageHandlers(Span messageHandlers) + { + messageHandlers[0] = MessageHandler.Get, T>(this); + } + + private void Run() + { + SpinWait waiter = new(); + int lastMessageVersion = 0; + while (true) + { + while (lastMessageVersion == messageVersion && !cts.IsCancellationRequested) + { + waiter.SpinOnce(); + } + + if (cts.IsCancellationRequested) + { + break; + } + + Thread.MemoryBarrier(); + T currentMessage = message; + lastMessageVersion = messageVersion; + + foreach (IListener system in systems) + { + system.Receive(ref currentMessage); + } + + waiter.Reset(); + finishedSignal = true; + } + } + + /// + /// Disposes the system group and finishes work on its thread. + /// + public void Dispose() + { + ThrowIfDisposed(); + + disposed = true; + cts.Cancel(); + if (thread.IsAlive) + { + thread.Join(); + } + + cts.Dispose(); + } + + /// + /// Adds the given to the group. + /// + public void Add(S system) where S : IListener + { + ThrowIfDisposed(); + + Array.Resize(ref systems, systems.Length + 1); + systems[^1] = system; + } + + /// + /// Removes the system of type from the group. + /// + public void Remove() where S : IListener + { + ThrowIfDisposed(); + + List> systemList = new(systems); + for (int i = 0; i < systemList.Count; i++) + { + if (systemList[i] is S system) + { + systemList.RemoveAt(i); + if (system is IDisposable disposable) + { + disposable.Dispose(); + } + + break; + } + } + + systems = systemList.ToArray(); + } + + /// + /// Signals the thread to process the given . + /// + public void Receive(ref T message) + { + ThrowIfDisposed(); + + finishedSignal = false; + this.message = message; + Thread.MemoryBarrier(); + Interlocked.Increment(ref messageVersion); + + SpinWait waiter = new(); + while (!finishedSignal) + { + waiter.SpinOnce(); + } + } + + [Conditional("DEBUG")] + private void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(nameof(SystemGroup), "This system group has already been disposed"); + } + } + } +} \ No newline at end of file diff --git a/core/buildTransitive/Simulation.Core.targets b/core/buildTransitive/Simulation.Core.targets new file mode 100644 index 0000000..64a0488 --- /dev/null +++ b/core/buildTransitive/Simulation.Core.targets @@ -0,0 +1,10 @@ + + + + + + $(MSBuildThisFileDirectory)..\lib\net9.0\$(Configuration)\Simulation.Core.dll + + + + \ No newline at end of file diff --git a/tests/MultithreadedTests.cs b/tests/MultithreadedTests.cs new file mode 100644 index 0000000..4d9252e --- /dev/null +++ b/tests/MultithreadedTests.cs @@ -0,0 +1,36 @@ +namespace Simulation.Tests +{ + public class MultithreadedTests : SimulationTests + { + [Test] + public void SystemGroupCreation() + { + SystemGroup group = new("update group"); + TimeSystem a = new(); + group.Add(a); + Simulator.Add(group); + Simulator.Broadcast(new UpdateMessage(0.1)); + Assert.That(a.time, Is.EqualTo(0.1)); + Simulator.Remove(group); + Simulator.Broadcast(new UpdateMessage(0.1)); + Assert.That(a.time, Is.EqualTo(0.1)); + group.Dispose(); + } + + [Test] + public void MultipleSystems() + { + SystemGroup group = new("update group"); + TimeSystem a = new(); + TimeSystem b = new(); + group.Add(a); + group.Add(b); + Simulator.Add(group); + Simulator.Broadcast(new UpdateMessage(0.1)); + Assert.That(a.time, Is.EqualTo(0.1)); + Assert.That(b.time, Is.EqualTo(0.1)); + Simulator.Remove(group); + group.Dispose(); + } + } +} \ No newline at end of file