diff --git a/Software/TS.NET/.gitignore b/Software/TS.NET/.gitignore
new file mode 100644
index 0000000..d4f7e25
--- /dev/null
+++ b/Software/TS.NET/.gitignore
@@ -0,0 +1,352 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+builds/
\ No newline at end of file
diff --git a/Software/TS.NET/LICENSE b/Software/TS.NET/LICENSE
new file mode 100644
index 0000000..9fc1046
--- /dev/null
+++ b/Software/TS.NET/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 macaba
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Software/TS.NET/README.md b/Software/TS.NET/README.md
new file mode 100644
index 0000000..4961dad
--- /dev/null
+++ b/Software/TS.NET/README.md
@@ -0,0 +1,5 @@
+# TS.NET
+
+[Thunderscope](https://github.com/EEVengers/ThunderScope)-compatible PC-host software written in C# using .NET 6 high-performing primitives & SIMD.
+
+![Flow diagram](docs/flow.png)
diff --git a/Software/TS.NET/build-scripts/TS.NET.Engine (win-x64).bat b/Software/TS.NET/build-scripts/TS.NET.Engine (win-x64).bat
new file mode 100644
index 0000000..98066ec
--- /dev/null
+++ b/Software/TS.NET/build-scripts/TS.NET.Engine (win-x64).bat
@@ -0,0 +1,12 @@
+@echo off
+
+echo Powershell path:
+where pwsh
+IF ERRORLEVEL 1 (
+ ECHO Powershell is not installed. Get it here: https://docs.microsoft.com/en-gb/powershell/scripting/install/installing-powershell
+ pause
+ EXIT /B
+)
+
+pwsh "win-x64/TS.NET.Engine (win-x64).ps1"
+pause
\ No newline at end of file
diff --git a/Software/TS.NET/build-scripts/TS.NET.UI.Avalonia (win-x64).bat b/Software/TS.NET/build-scripts/TS.NET.UI.Avalonia (win-x64).bat
new file mode 100644
index 0000000..6f8d192
--- /dev/null
+++ b/Software/TS.NET/build-scripts/TS.NET.UI.Avalonia (win-x64).bat
@@ -0,0 +1,12 @@
+@echo off
+
+echo Powershell path:
+where pwsh
+IF ERRORLEVEL 1 (
+ ECHO Powershell is not installed. Get it here: https://docs.microsoft.com/en-gb/powershell/scripting/install/installing-powershell
+ pause
+ EXIT /B
+)
+
+pwsh "win-x64/TS.NET.UI.Avalonia (win-x64).ps1"
+pause
\ No newline at end of file
diff --git a/Software/TS.NET/build-scripts/win-x64/TS.NET.Engine (win-x64).ps1 b/Software/TS.NET/build-scripts/win-x64/TS.NET.Engine (win-x64).ps1
new file mode 100644
index 0000000..fa9cf98
--- /dev/null
+++ b/Software/TS.NET/build-scripts/win-x64/TS.NET.Engine (win-x64).ps1
@@ -0,0 +1,24 @@
+New-Variable -Name "projectFolder" -Value (Join-Path (Resolve-Path ..) 'source/TS.NET.Engine')
+$xml = [Xml] (Get-Content $projectFolder\TS.NET.Engine.csproj)
+$version = [Version] $xml.Project.PropertyGroup.Version
+New-Variable -Name "publishFolder" -Value (Join-Path (Resolve-Path ..) 'builds/win-x64/TS.NET.Engine' $version)
+
+# Remove destination folder if exists
+if(Test-Path $publishFolder -PathType Container) {
+ rm -r $publishFolder
+}
+
+# Publish application
+Write-Host "Publishing project..." -ForegroundColor yellow
+Write-Host -> $publishFolder -ForegroundColor DarkYellow
+dotnet publish $projectFolder/TS.NET.Engine.csproj -r win-x64 -c Release --self-contained /p:PublishSingleFile=true /p:PublishTrimmed=true /p:IncludeNativeLibrariesForSelfExtract=true --output $publishFolder
+if ($LastExitCode -ne 0) { break }
+Write-Host ""
+
+# Remove debug files
+rm $publishFolder/*.pdb
+
+# Compress-Archive -Force -Path $publishFolder\* -DestinationPath $publishFolder/../TS.NET.Engine_win-x64_v$version.zip
+
+Write-Host Build Complete -ForegroundColor green
+Write-Host -> $publishFolder -ForegroundColor DarkYellow
\ No newline at end of file
diff --git a/Software/TS.NET/build-scripts/win-x64/TS.NET.UI.Avalonia (win-x64).ps1 b/Software/TS.NET/build-scripts/win-x64/TS.NET.UI.Avalonia (win-x64).ps1
new file mode 100644
index 0000000..e1eef43
--- /dev/null
+++ b/Software/TS.NET/build-scripts/win-x64/TS.NET.UI.Avalonia (win-x64).ps1
@@ -0,0 +1,24 @@
+New-Variable -Name "projectFolder" -Value (Join-Path (Resolve-Path ..) 'source/TS.NET.UI.Avalonia')
+$xml = [Xml] (Get-Content $projectFolder\TS.NET.UI.Avalonia.csproj)
+$version = [Version] $xml.Project.PropertyGroup.Version
+New-Variable -Name "publishFolder" -Value (Join-Path (Resolve-Path ..) 'builds/win-x64/TS.NET.UI.Avalonia' $version)
+
+# Remove destination folder if exists
+if(Test-Path $publishFolder -PathType Container) {
+ rm -r $publishFolder
+}
+
+# Publish application
+Write-Host "Publishing project..." -ForegroundColor yellow
+Write-Host -> $publishFolder -ForegroundColor DarkYellow
+dotnet publish $projectFolder/TS.NET.UI.Avalonia.csproj -r win-x64 -c Release --self-contained /p:PublishSingleFile=true /p:PublishTrimmed=true /p:IncludeNativeLibrariesForSelfExtract=true --output $publishFolder
+if ($LastExitCode -ne 0) { break }
+Write-Host ""
+
+# Remove debug files
+rm $publishFolder/*.pdb
+
+# Compress-Archive -Force -Path $publishFolder\* -DestinationPath $publishFolder/../TS.NET.UI.Avalonia_win-x64_v$version.zip
+
+Write-Host Build Complete -ForegroundColor green
+Write-Host -> $publishFolder -ForegroundColor DarkYellow
\ No newline at end of file
diff --git a/Software/TS.NET/docs/flow.png b/Software/TS.NET/docs/flow.png
new file mode 100644
index 0000000..5ee5c63
Binary files /dev/null and b/Software/TS.NET/docs/flow.png differ
diff --git a/Software/TS.NET/source/PlayingWithShaders/OscilloscopeDisplay.cs b/Software/TS.NET/source/PlayingWithShaders/OscilloscopeDisplay.cs
new file mode 100644
index 0000000..f8050a2
--- /dev/null
+++ b/Software/TS.NET/source/PlayingWithShaders/OscilloscopeDisplay.cs
@@ -0,0 +1,132 @@
+using Microsoft.Extensions.Logging;
+using ObjectTK;
+using ObjectTK.GLObjects;
+using OpenTK.Graphics.OpenGL4;
+using OpenTK.Mathematics;
+using OpenTK.Windowing.Common;
+using OpenTK.Windowing.Desktop;
+using OpenTK.Windowing.GraphicsLibraryFramework;
+using System;
+using TS.NET;
+
+// https://github.com/opentk/LearnOpenTK/blob/master/Chapter1/2-HelloTriangle/Window.cs
+// https://github.com/jbentham/streaming/blob/main/rpi_opengl_graph.c
+// https://iosoft.blog/2020/12/15/oscilloscope-display-opengl-raspberry-pi/
+// https://vitaliburkov.wordpress.com/2016/09/17/simple-and-fast-high-quality-antialiased-lines-with-opengl/
+
+namespace PlayingWithShaders
+{
+ public class OscilloscopeDisplay : GameWindow
+ {
+ private ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
+ private ShaderProgram shader;
+ ThunderscopeBridgeReader bridge;
+ IInterprocessSemaphoreWaiter bridgeReadSemaphore;
+ int vbo;
+ int vao;
+
+ public OscilloscopeDisplay(GameWindowSettings gameWindowSettings, NativeWindowSettings nativeWindowSettings) : base(gameWindowSettings, nativeWindowSettings) { }
+
+ protected override void OnLoad()
+ {
+ base.OnLoad();
+
+ uint bufferLength = 4 * 100 * 1000 * 1000; //Maximum record length = 100M samples per channel
+ bridge = new(new ThunderscopeBridgeOptions("ThunderScope.1", bufferLength), loggerFactory);
+ bridgeReadSemaphore = bridge.GetReaderSemaphore();
+ int channelLength = bridge.Configuration.ChannelLength;
+
+ shader = GLFactory.Shader.EmbeddedResVertFrag("Graph", "graph2.vertex.glsl", "graph.fragment.glsl");
+ GL.Hint(HintTarget.LineSmoothHint, HintMode.Nicest);
+ GL.Enable(EnableCap.Blend);
+ GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+ GL.ClearColor(new Color4(0x21, 0x21, 0x21, 0xff));
+
+ //====== var buffer = GLFactory.Buffer.ArrayBuffer("Channel1", graph); ======
+ vbo = GL.GenBuffer();
+ //var label = $"Buffer: {name}";
+ GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
+ //GL.ObjectLabel(ObjectLabelIdentifier.Buffer, vbo, label.Length, label);
+ int elemSize;
+ unsafe
+ {
+ elemSize = sizeof(byte);
+ }
+ //GL.BufferData(BufferTarget.ArrayBuffer, elemSize * graph.Length, graph, BufferUsageHint.DynamicRead);
+ GL.BufferData(BufferTarget.ArrayBuffer, elemSize * channelLength, bridge.DataPointer, BufferUsageHint.DynamicRead);
+ GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
+ // ============================================================
+
+
+ // ====== vertexArray = GLFactory.VertexArray.FromBuffers("Channel1", buffer); ======
+ vao = GL.GenVertexArray();
+ //var label = $"VertexArray: {name}";
+ GL.BindVertexArray(vao);
+ //GL.ObjectLabel(ObjectLabelIdentifier.VertexArray, vao, name.Length, label);
+
+ GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
+ GL.EnableVertexAttribArray(0);
+ //GL.VertexAttribPointer(0, 1, VertexAttribPointerType.Int, false, elemSize, 0);
+ GL.VertexAttribIPointer(0, 1, VertexAttribIntegerType.UnsignedByte, elemSize, IntPtr.Zero);
+
+ // clean up:
+ GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
+ GL.BindVertexArray(0);
+ //=========================
+ }
+
+ protected unsafe override void OnRenderFrame(FrameEventArgs args)
+ {
+ base.OnRenderFrame(args);
+
+ if (bridgeReadSemaphore.Wait(500))
+ {
+ int channelLength = bridge.Configuration.ChannelLength;
+
+
+ GL.UseProgram(shader.Handle);
+
+ //GL.Uniform1(shader.Uniforms["ScaleX"].Location, 1.0f);
+ //GL.Uniform1(shader.Uniforms["OffsetX"].Location, 0.0f);
+ var scale = 1.0f / 128.0f;
+ GL.Uniform1(shader.Uniforms["ScaleY"].Location, scale);
+ GL.Uniform1(shader.Uniforms["OffsetY"].Location, -1f); //The scaling required to map input space to opengl space
+ GL.Uniform4(shader.Uniforms["Color"].Location, Color4.Red);
+
+ GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
+ GL.BufferData(BufferTarget.ArrayBuffer, 1 * channelLength, bridge.DataPointer, BufferUsageHint.DynamicRead);
+ GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
+
+ GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
+
+ //GL.BindVertexArray(vertexArray.Handle);
+ GL.BindVertexArray(vao);
+ // This is old OpenGL. Consider modern OpenGL at some point https://stackoverflow.com/questions/3484260/opengl-line-width
+ GL.LineWidth(1.0f);
+ GL.DrawArrays(PrimitiveType.LineStrip, 0, channelLength);
+ GL.BindVertexArray(0);
+
+ GL.UseProgram(0);
+ SwapBuffers();
+ bridge.DataRead();
+ }
+ }
+
+ protected override void OnUpdateFrame(FrameEventArgs e)
+ {
+ base.OnUpdateFrame(e);
+
+ if (KeyboardState.IsKeyDown(Keys.Escape))
+ {
+ Close();
+ }
+ }
+
+ protected override void OnResize(ResizeEventArgs e)
+ {
+ base.OnResize(e);
+
+ GL.Viewport(0, 0, Size.X, Size.Y);
+ }
+ }
+}
diff --git a/Software/TS.NET/source/PlayingWithShaders/PlayingWithShaders.csproj b/Software/TS.NET/source/PlayingWithShaders/PlayingWithShaders.csproj
new file mode 100644
index 0000000..c18413b
--- /dev/null
+++ b/Software/TS.NET/source/PlayingWithShaders/PlayingWithShaders.csproj
@@ -0,0 +1,41 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Software/TS.NET/source/PlayingWithShaders/Primitives.cs b/Software/TS.NET/source/PlayingWithShaders/Primitives.cs
new file mode 100644
index 0000000..2c2638e
--- /dev/null
+++ b/Software/TS.NET/source/PlayingWithShaders/Primitives.cs
@@ -0,0 +1,33 @@
+using System;
+using ObjectTK;
+using ObjectTK.GLObjects;
+using OpenTK.Mathematics;
+
+namespace PlayingWithShaders {
+ public static class Primitives {
+
+ private static readonly Vector2[] quadVertices = {
+ new Vector2(-1, -1),
+ new Vector2(-1, 1),
+ new Vector2( 1, 1),
+ new Vector2( 1, -1),
+ };
+
+ private static readonly int[] quadIndices = {
+ 2, 1, 0,
+ 2, 3, 0
+ };
+
+ private static readonly Lazy> _quadPositionBuffer = new Lazy>(() => GLFactory.Buffer.ArrayBuffer("Quad Positions", quadVertices));
+ private static readonly Lazy> _quadIndexBuffer = new Lazy>(() => GLFactory.Buffer.ArrayBuffer("Quad Indices", quadIndices));
+ private static readonly Lazy _quadVertexArray =
+ new Lazy(() =>
+ GLFactory.VertexArray.IndexAndVertexBuffers("Quad", _quadIndexBuffer.Value, _quadPositionBuffer.Value)
+ );
+
+ /// A vertex array with a simple quad from (-1.-1) to (1,1).
+ public static VertexArray Quad => _quadVertexArray.Value;
+
+
+ }
+}
diff --git a/Software/TS.NET/source/PlayingWithShaders/Program.cs b/Software/TS.NET/source/PlayingWithShaders/Program.cs
new file mode 100644
index 0000000..a0324cf
--- /dev/null
+++ b/Software/TS.NET/source/PlayingWithShaders/Program.cs
@@ -0,0 +1,91 @@
+// https://www.shadertoy.com/view/slX3W2
+// https://iquilezles.org/www/articles/distance/distance.htm
+
+using OpenTK.Graphics.OpenGL4;
+using OpenTK.Mathematics;
+using OpenTK.Windowing.Common;
+using OpenTK.Windowing.Desktop;
+using PlayingWithShaders;
+
+
+Console.WriteLine("Hello, World!");
+
+NativeWindowSettings nativeWindowSettings = new NativeWindowSettings
+{
+ Flags = ContextFlags.Debug,
+ Profile = ContextProfile.Core,
+ Title = "PlayingWithShaders",
+ NumberOfSamples = 0,
+ Size = new Vector2i(1024, 768),
+ //APIVersion = new Version(4, 2),
+};
+
+using (var window = new OscilloscopeDisplay(GameWindowSettings.Default, nativeWindowSettings))
+{
+ //GLDebugLog.Message += OnMessage;
+ //window.RenderFrame += OnRenderFrame;
+ //window.UpdateFrame += OnUpdate;
+ //Thread.Sleep(3000);
+ window.Run();
+}
+
+
+//void OnUpdate(FrameEventArgs obj)
+//{
+// var r = new Random();
+// var seriesIdx = r.Next(_graph.State.Series.Count);
+// var series = _graph.State.Series[seriesIdx];
+// var (x, y) = ScatterGraphGenerator.GenNormalDistPt(r);
+// var pt = DateTime.UtcNow.Ticks;
+// var str = pt.ToString();
+// var offset = series.Points.Count;
+// series.Add(str, x, y);
+// _graph.State.Update((float)obj.Time);
+//}
+
+//void OnMessage(object sender, DebugMessageEventArgs e)
+//{
+// Console.Error.WriteLine($"[{e.ID}]{e.Severity}|{e.Type}/{e.Source}: {e.Message}");
+//}
+
+//void OnRenderFrame(FrameEventArgs frameEventArgs)
+//{
+// var cur = _timer.Elapsed;
+// var delta = cur - _lastFrame;
+// _lastFrame = cur;
+// GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
+// GL.Viewport(0, 0, _window.ClientSize.X, _window.ClientSize.Y);
+// var aspect = _window.ClientSize.X / (float)_window.ClientSize.Y;
+// _graph.State.Camera.Current.AspectRatio = aspect;
+// _graph.State.Camera.Target.AspectRatio = aspect;
+// _graph.Render();
+
+// _window.Context.SwapBuffers();
+//}
+
+//double onUpdateTime = 1;
+
+//GLDebugLog.Message += OnMessage;
+//window.RenderFrame += OnRenderFrame;
+//window.UpdateFrame += OnUpdate;
+//window.Run();
+
+//void OnMessage(object sender, DebugMessageEventArgs e)
+//{
+// Console.Error.WriteLine($"[{e.ID}]{e.Severity}|{e.Type}/{e.Source}: {e.Message}");
+//}
+
+//void OnRenderFrame(FrameEventArgs frameEventArgs)
+//{
+// GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
+// GL.Viewport(0, 0, window.ClientSize.X, window.ClientSize.Y);
+
+
+// window.Context.SwapBuffers();
+// window.Title = $"OnRenderFrame: {(1.0 / frameEventArgs.Time):F2} OnUpdate: {(1.0 / onUpdateTime):F2}";
+//}
+
+//void OnUpdate(FrameEventArgs frameEventArgs)
+//{
+// onUpdateTime = frameEventArgs.Time;
+//}
\ No newline at end of file
diff --git a/Software/TS.NET/source/PlayingWithShaders/ShaderExtensions.cs b/Software/TS.NET/source/PlayingWithShaders/ShaderExtensions.cs
new file mode 100644
index 0000000..8c1f04f
--- /dev/null
+++ b/Software/TS.NET/source/PlayingWithShaders/ShaderExtensions.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using ObjectTK;
+using ObjectTK.GLObjects;
+
+namespace PlayingWithShaders {
+ public static class ShaderExtensions {
+
+ private static readonly Dictionary _readCache = new Dictionary();
+
+ private static string LoadFromRes(string name) {
+ if (_readCache.TryGetValue(name, out var res)) {
+ return res;
+ }
+ // retrieves THIS assembly, not the one that started the process.
+ // in this way, it's always certain to load from here.
+ var assembly = Assembly.GetExecutingAssembly();
+ var resources = assembly.GetManifestResourceNames();
+ var resourceName = resources.Single(s => s.EndsWith(name));
+
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ using var reader = new StreamReader(stream!);
+ var result = reader.ReadToEnd();
+ _readCache[name] = result;
+ return result;
+ }
+
+ /// Loads a vertex and fragment shader from an embedded resource in the executing assembly.
+ public static ShaderProgram EmbeddedResVertFrag(this GLShaderFactory fact, string name, string vertName, string fragName) {
+ var vertSrc = LoadFromRes(vertName);
+ var fragSrc = LoadFromRes(fragName);
+
+ return fact.VertexFrag(name, vertSrc, fragSrc);
+ }
+
+ }
+}
diff --git a/Software/TS.NET/source/PlayingWithShaders/Shaders/graph.fragment.glsl b/Software/TS.NET/source/PlayingWithShaders/Shaders/graph.fragment.glsl
new file mode 100644
index 0000000..cae9fa4
--- /dev/null
+++ b/Software/TS.NET/source/PlayingWithShaders/Shaders/graph.fragment.glsl
@@ -0,0 +1,10 @@
+#version 330
+
+uniform vec4 Color;
+
+//out vec4 FragColor;
+
+void main()
+{
+ gl_FragColor = Color;//vec4(1.0, 1.0, 1.0, 1.0);
+}
\ No newline at end of file
diff --git a/Software/TS.NET/source/PlayingWithShaders/Shaders/graph.vertex.glsl b/Software/TS.NET/source/PlayingWithShaders/Shaders/graph.vertex.glsl
new file mode 100644
index 0000000..f8aa99a
--- /dev/null
+++ b/Software/TS.NET/source/PlayingWithShaders/Shaders/graph.vertex.glsl
@@ -0,0 +1,16 @@
+#version 330 core
+
+layout(location = 0) in vec2 InPosition;
+
+uniform int PointCount;
+uniform float OffsetX;
+uniform float ScaleX;
+uniform float OffsetY;
+uniform float ScaleY;
+
+void main(void) {
+ float dX = 2/PointCount;
+
+ gl_Position = vec4((InPosition.x * ScaleX) + OffsetX, (InPosition.y * ScaleY) + OffsetY, 0, 1.0);
+ //gl_PointSize = max(1.0, sprite);
+}
diff --git a/Software/TS.NET/source/PlayingWithShaders/Shaders/graph2.vertex.glsl b/Software/TS.NET/source/PlayingWithShaders/Shaders/graph2.vertex.glsl
new file mode 100644
index 0000000..9e8b3b1
--- /dev/null
+++ b/Software/TS.NET/source/PlayingWithShaders/Shaders/graph2.vertex.glsl
@@ -0,0 +1,17 @@
+#version 330 core
+
+layout(location = 0) in uint InPosition;
+
+uniform int PointCount;
+//uniform float OffsetX;
+//uniform float ScaleX;
+uniform float OffsetY;
+uniform float ScaleY;
+
+void main(void) {
+ //float dX = 2/PointCount;
+
+ //gl_Position = vec4(-1 + (InPosition* 0.0078125f), (InPosition + OffsetY) * ScaleY, 0, 1.0);
+ gl_Position = vec4(-1 + (gl_VertexID* 0.0000002f), (InPosition * ScaleY) + OffsetY, 0, 1.0);
+ //gl_PointSize = max(1.0, sprite);
+}
diff --git a/Software/TS.NET/source/TS.NET.Benchmarks/CpuDiagnoser.cs b/Software/TS.NET/source/TS.NET.Benchmarks/CpuDiagnoser.cs
new file mode 100644
index 0000000..5eec6e0
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Benchmarks/CpuDiagnoser.cs
@@ -0,0 +1,106 @@
+using BenchmarkDotNet.Analysers;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Engines;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Loggers;
+using BenchmarkDotNet.Reports;
+using BenchmarkDotNet.Running;
+using BenchmarkDotNet.Validators;
+using System;
+using System.Diagnostics;
+
+namespace TS.NET.Benchmark
+{
+ public class CpuDiagnoserAttribute : Attribute, IConfigSource
+ {
+ public IConfig Config { get; }
+
+ public CpuDiagnoserAttribute()
+ {
+ Config = ManualConfig.CreateEmpty().AddDiagnoser(new CpuDiagnoser());
+ }
+ }
+
+ public class CpuDiagnoser : IDiagnoser
+ {
+ Process proc;
+
+ public CpuDiagnoser()
+ {
+ this.proc = Process.GetCurrentProcess();
+ }
+
+ public IEnumerable Ids => new[] { "CPU" };
+
+ public IEnumerable Exporters => Array.Empty();
+
+ public IEnumerable Analysers => Array.Empty();
+
+ public void DisplayResults(ILogger logger)
+ {
+ }
+
+ public RunMode GetRunMode(BenchmarkCase benchmarkCase)
+ {
+ return RunMode.NoOverhead;
+ }
+
+ long userStart, userEnd;
+ long privStart, privEnd;
+
+ public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
+ {
+ if (signal == HostSignal.BeforeActualRun)
+ {
+ userStart = proc.UserProcessorTime.Ticks;
+ privStart = proc.PrivilegedProcessorTime.Ticks;
+ }
+ if (signal == HostSignal.AfterActualRun)
+ {
+ userEnd = proc.UserProcessorTime.Ticks;
+ privEnd = proc.PrivilegedProcessorTime.Ticks;
+ }
+ }
+
+ public IEnumerable ProcessResults(DiagnoserResults results)
+ {
+ yield return new Metric(CpuUserMetricDescriptor.Instance, (userEnd - userStart) * 100d / results.TotalOperations);
+ yield return new Metric(CpuPrivilegedMetricDescriptor.Instance, (privEnd - privStart) * 100d / results.TotalOperations);
+ }
+
+ public IEnumerable Validate(ValidationParameters validationParameters)
+ {
+ yield break;
+ }
+
+ class CpuUserMetricDescriptor : IMetricDescriptor
+ {
+ internal static readonly IMetricDescriptor Instance = new CpuUserMetricDescriptor();
+
+ public string Id => "CPU User Time";
+ public string DisplayName => Id;
+ public string Legend => Id;
+ public string NumberFormat => "0.##";
+ public UnitType UnitType => UnitType.Time;
+ public string Unit => "ns";
+ public bool TheGreaterTheBetter => false;
+ public int PriorityInCategory => 1;
+ }
+
+ class CpuPrivilegedMetricDescriptor : IMetricDescriptor
+ {
+ internal static readonly IMetricDescriptor Instance = new CpuPrivilegedMetricDescriptor();
+
+ public string Id => "CPU Privileged Time";
+ public string DisplayName => Id;
+ public string Legend => Id;
+ public string NumberFormat => "0.##";
+ public UnitType UnitType => UnitType.Time;
+ public string Unit => "ns";
+ public bool TheGreaterTheBetter => false;
+ public int PriorityInCategory => 1;
+ }
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.Benchmarks/PipelineBenchmark.cs b/Software/TS.NET/source/TS.NET.Benchmarks/PipelineBenchmark.cs
new file mode 100644
index 0000000..aa4633c
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Benchmarks/PipelineBenchmark.cs
@@ -0,0 +1,55 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+
+namespace TS.NET.Benchmark
+{
+ [SimpleJob(RuntimeMoniker.Net60)]
+ [MemoryDiagnoser]
+ public class PipelineBenchmark
+ {
+ private const int samplingRate = 1000000000;
+ private const int byteBufferSize = 8000000;
+ private readonly Memory input = new byte[byteBufferSize];
+ private readonly Memory shuffleOutput = new byte[byteBufferSize];
+ private readonly Memory triggerBuffer = new ulong[byteBufferSize / 64];
+ private readonly RisingEdgeTrigger trigger1 = new(200, 190, 1000);
+ private readonly RisingEdgeTrigger trigger2 = new(200, 190, 1000);
+ private readonly RisingEdgeTrigger trigger3 = new(200, 190, 1000);
+ private readonly RisingEdgeTrigger trigger4 = new(200, 190, 1000);
+ private Memory channel1;
+ private Memory channel2;
+ private Memory channel3;
+ private Memory channel4;
+ Memory triggerChannel1;
+ Memory triggerChannel2;
+ Memory triggerChannel3;
+ Memory triggerChannel4;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ Waveforms.FourChannelSine(input.Span, samplingRate, 1000);
+ channel1 = shuffleOutput.Slice(0, 2000000);
+ channel2 = shuffleOutput.Slice(2000000, 2000000);
+ channel3 = shuffleOutput.Slice(4000000, 2000000);
+ channel4 = shuffleOutput.Slice(6000000, 2000000);
+ triggerChannel1 = triggerBuffer.Slice(0, 31250);
+ triggerChannel2 = triggerBuffer.Slice(31250, 31250);
+ triggerChannel3 = triggerBuffer.Slice(31250 * 2, 31250);
+ triggerChannel4 = triggerBuffer.Slice(31250 * 3, 31250);
+ }
+
+ [Benchmark(Description = "4 channels")]
+ public void FourChannelPipeline()
+ {
+ for (int i = 0; i < 125; i++)
+ {
+ Shuffle.FourChannels(input.Span, shuffleOutput.Span);
+ trigger1.ProcessSimd(input: channel1.Span, trigger: triggerChannel1.Span);
+ trigger2.ProcessSimd(input: channel2.Span, trigger: triggerChannel2.Span);
+ trigger3.ProcessSimd(input: channel3.Span, trigger: triggerChannel3.Span);
+ trigger4.ProcessSimd(input: channel4.Span, trigger: triggerChannel4.Span);
+ }
+ }
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.Benchmarks/Program.cs b/Software/TS.NET/source/TS.NET.Benchmarks/Program.cs
new file mode 100644
index 0000000..648846b
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Benchmarks/Program.cs
@@ -0,0 +1,12 @@
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Running;
+using TS.NET.Benchmark;
+
+DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary);
+//_ = BenchmarkRunner.Run(typeof(Program).Assembly);
+//_ = BenchmarkRunner.Run();
+//_ = BenchmarkRunner.Run();
+//_ = BenchmarkRunner.Run();
+_ = BenchmarkRunner.Run();
+_ = BenchmarkRunner.Run();
+Console.ReadKey();
\ No newline at end of file
diff --git a/Software/TS.NET/source/TS.NET.Benchmarks/RisingEdgeTriggerBenchmark.cs b/Software/TS.NET/source/TS.NET.Benchmarks/RisingEdgeTriggerBenchmark.cs
new file mode 100644
index 0000000..601cab1
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Benchmarks/RisingEdgeTriggerBenchmark.cs
@@ -0,0 +1,68 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Jobs;
+
+namespace TS.NET.Benchmark
+{
+ [SimpleJob(RuntimeMoniker.Net60)]
+ [MemoryDiagnoser]
+ //[CpuDiagnoser]
+ //[InProcess]
+ //[HardwareCounters(HardwareCounter.TotalIssues)]
+ public class RisingEdgeTriggerBenchmark
+ {
+ private const int samplingRate = 1000000000;
+ private const int byteBufferSize = 8000000;
+ private readonly Memory buffer1MHz = new byte[byteBufferSize];
+ private readonly Memory buffer1KHz = new byte[byteBufferSize];
+ private readonly Memory triggerBufferU64 = new ulong[byteBufferSize / 64];
+ private readonly RisingEdgeTrigger trigger = new(200, 190, 1000);
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ Waveforms.Sine(buffer1MHz.Span, samplingRate, 1000000);
+ Waveforms.Sine(buffer1KHz.Span, samplingRate, 1000);
+ }
+
+ //[Benchmark(Description = "Rising edge with hysteresis (10 counts) & holdoff (1us) and no SIMD, 1KHz sine (125 x 8MS)")]
+ //public void RisingEdge2()
+ //{
+ // for (int i = 0; i < 125; i++)
+ // trigger.RisingEdge(buffer1KHz.Span, triggerBufferU64.Span);
+ //}
+
+ [Benchmark(Description = "Rising edge with hysteresis (10 counts), holdoff (1us) & SIMD, 1KHz sine (125 x 8MS)")]
+ public void RisingEdge1()
+ {
+ trigger.Reset(200, 190, 1000);
+ for (int i = 0; i < 125; i++)
+ trigger.ProcessSimd(buffer1KHz.Span, triggerBufferU64.Span);
+ }
+
+ // 0.18 CPU cycles per sample
+ [Benchmark(Description = "Rising edge with hysteresis (10 counts), holdoff (1us) & SIMD, 1MHz sine (125 x 8MS)")]
+ public void RisingEdge2()
+ {
+ trigger.Reset(200, 190, 1000);
+ for (int i = 0; i < 125; i++)
+ trigger.ProcessSimd(buffer1MHz.Span, triggerBufferU64.Span);
+ }
+
+ //[Benchmark(Description = "Rising edge with hysteresis (10 counts), holdoff (1ms) & SIMD, 1KHz sine (125 x 8MS)")]
+ //public void RisingEdge3()
+ //{
+ // trigger.Reset(200, 190, 1000000);
+ // for (int i = 0; i < 125; i++)
+ // trigger.ProcessSimd(buffer1KHz.Span, triggerBufferU64.Span);
+ //}
+
+ //[Benchmark(Description = "Rising edge with hysteresis (10 counts), holdoff (1ms) & SIMD, 1MHz sine (125 x 8MS)")]
+ //public void RisingEdge4()
+ //{
+ // trigger.Reset(200, 190, 1000000);
+ // for (int i = 0; i < 125; i++)
+ // trigger.ProcessSimd(buffer1MHz.Span, triggerBufferU64.Span);
+ //}
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.Benchmarks/ShuffleBenchmark.cs b/Software/TS.NET/source/TS.NET.Benchmarks/ShuffleBenchmark.cs
new file mode 100644
index 0000000..aaf35ac
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Benchmarks/ShuffleBenchmark.cs
@@ -0,0 +1,36 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+
+namespace TS.NET.Benchmark
+{
+ [SimpleJob(RuntimeMoniker.Net60)]
+ [MemoryDiagnoser]
+ //[CpuDiagnoser]
+ //[InProcess]
+ public class ShuffleBenchmark
+ {
+ private const int byteBufferSize = 8000000;
+ private readonly Memory input = new byte[byteBufferSize];
+ private readonly Memory output = new byte[byteBufferSize];
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ Waveforms.FourChannelCount(input.Span);
+ }
+
+ [Benchmark(Description = "Four channel shuffle (125 x 8MS)")] // 0.40 CPU cycles per sample
+ public void FourChannels()
+ {
+ for (int i = 0; i < 125; i++)
+ Shuffle.FourChannels(input.Span, output.Span);
+ }
+
+ [Benchmark(Description = "Two channel shuffle (125 x 8MS)")] // 0.32 CPU cycles per sample
+ public void TwoChannels()
+ {
+ for (int i = 0; i < 125; i++)
+ Shuffle.TwoChannels(input.Span, output.Span);
+ }
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.Benchmarks/SumU8ToI32Benchmark.cs b/Software/TS.NET/source/TS.NET.Benchmarks/SumU8ToI32Benchmark.cs
new file mode 100644
index 0000000..f4d8d73
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Benchmarks/SumU8ToI32Benchmark.cs
@@ -0,0 +1,49 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+
+namespace TS.NET.Benchmark
+{
+ [SimpleJob(RuntimeMoniker.Net60)]
+ //[MemoryDiagnoser]
+ public class SumU8toI32Benchmark
+ {
+ private const int byteBufferSize = 8000000;
+ private readonly Memory input = new byte[byteBufferSize];
+ private readonly Memory bufferI16 = new short[byteBufferSize / 2];
+ private readonly Memory bufferI32 = new int[byteBufferSize / 2];
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ Waveforms.Oversampling_1Channel_2Avg(input.Span);
+ }
+
+ [Benchmark(Description = "I32: Sum by 2 (1GS -> 500MS)", Baseline = true)] // CPU cycles per sample
+ public void I32_2_()
+ {
+ for (int i = 0; i < 125; i++)
+ HorizontalSum.U8ToI32(input.Span, bufferI16.Span, bufferI32.Span, 1);
+ }
+
+ [Benchmark(Description = "I32: Sum by 4 (1GS -> 250MS)")] // CPU cycles per sample
+ public void I32_4_()
+ {
+ for (int i = 0; i < 125; i++)
+ HorizontalSum.U8ToI32(input.Span, bufferI16.Span, bufferI32.Span, 2);
+ }
+
+ [Benchmark(Description = "I32: Sum by 8 (1GS -> 125MS)")] // CPU cycles per sample
+ public void I32_8_()
+ {
+ for (int i = 0; i < 125; i++)
+ HorizontalSum.U8ToI32(input.Span, bufferI16.Span, bufferI32.Span, 3);
+ }
+
+ //[Benchmark(Description = "I32: Sum by 16 (1GS -> 62.5MS)")] // CPU cycles per sample
+ //public void I32_16_()
+ //{
+ // for (int i = 0; i < 125; i++)
+ // Sum.U8ToI32(input.Span, bufferI16.Span, bufferI32.Span, 4);
+ //}
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.Benchmarks/SumU8toI16Benchmark.cs b/Software/TS.NET/source/TS.NET.Benchmarks/SumU8toI16Benchmark.cs
new file mode 100644
index 0000000..076dbac
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Benchmarks/SumU8toI16Benchmark.cs
@@ -0,0 +1,70 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+
+namespace TS.NET.Benchmark
+{
+ [SimpleJob(RuntimeMoniker.Net60)]
+ //[MemoryDiagnoser]
+ public class SumU8toI16Benchmark
+ {
+ private const int byteBufferSize = 8000000;
+ private readonly Memory input = new byte[byteBufferSize];
+ private readonly Memory bufferI16 = new short[byteBufferSize / 2];
+ private readonly Memory bufferI32 = new int[byteBufferSize / 2];
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ Waveforms.Oversampling_1Channel_2Avg(input.Span);
+ }
+
+ [Benchmark(Description = "I16: Sum by 2 (1GS -> 500MS)", Baseline = true)] // x CPU cycles per sample
+ public void I16_2()
+ {
+ for (int i = 0; i < 125; i++)
+ HorizontalSum.U8ToI16(input.Span, bufferI16.Span, 1);
+ }
+
+ [Benchmark(Description = "I16: Sum by 4 (1GS -> 250MS)")] // x CPU cycles per sample
+ public void I16_4()
+ {
+ for (int i = 0; i < 125; i++)
+ HorizontalSum.U8ToI16(input.Span, bufferI16.Span, 2);
+ }
+
+ [Benchmark(Description = "I16: Sum by 8 (1GS -> 125MS)")] // x CPU cycles per sample
+ public void I16_8()
+ {
+ for (int i = 0; i < 125; i++)
+ HorizontalSum.U8ToI16(input.Span, bufferI16.Span, 3);
+ }
+
+ //[Benchmark(Description = "I16: Sum by 16 (1GS -> 62.5MS)")] // x CPU cycles per sample
+ //public void I16_16()
+ //{
+ // for (int i = 0; i < 125; i++)
+ // Sum.U8ToI16(input.Span, bufferI16.Span, 4);
+ //}
+
+ //[Benchmark(Description = "I16: Sum by 32 (1GS -> 31.25MS)")] // x CPU cycles per sample
+ //public void I16_32()
+ //{
+ // for (int i = 0; i < 125; i++)
+ // Sum.U8ToI16(input.Span, bufferI16.Span, 5);
+ //}
+
+ //[Benchmark(Description = "I16: Sum by 64 (1GS -> 15.625MS)")] // x CPU cycles per sample
+ //public void I16_64()
+ //{
+ // for (int i = 0; i < 125; i++)
+ // Sum.U8ToI16(input.Span, bufferI16.Span, 6);
+ //}
+
+ //[Benchmark(Description = "I16: Sum by 128 (1GS -> 7.8125MS)")] // x CPU cycles per sample
+ //public void I16_128()
+ //{
+ // for (int i = 0; i < 125; i++)
+ // Sum.U8ToI16(input.Span, bufferI16.Span, 7);
+ //}
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.Benchmarks/TS.NET.Benchmarks.csproj b/Software/TS.NET/source/TS.NET.Benchmarks/TS.NET.Benchmarks.csproj
new file mode 100644
index 0000000..2a6e3c4
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Benchmarks/TS.NET.Benchmarks.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Software/TS.NET/source/TS.NET.Engine/Program.cs b/Software/TS.NET/source/TS.NET.Engine/Program.cs
new file mode 100644
index 0000000..8898cd8
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Engine/Program.cs
@@ -0,0 +1,28 @@
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using TS.NET;
+using TS.NET.Engine;
+
+Console.Title = "Engine";
+using (Process p = Process.GetCurrentProcess())
+ p.PriorityClass = ProcessPriorityClass.High;
+
+using var loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(options => { options.SingleLine = true; options.TimestampFormat = "HH:mm:ss "; }).AddFilter(level => level >= LogLevel.Debug));
+
+BlockingChannel memoryPool = new();
+for (int i = 0; i < 120; i++) // 120 = about 1 seconds worth of samples at 1GSPS
+ memoryPool.Writer.Write(new ThunderscopeMemory());
+
+Thread.Sleep(1000);
+
+BlockingChannel processingPool = new();
+ProcessingTask processingTask = new();
+processingTask.Start(loggerFactory, processingPool.Reader, memoryPool.Writer);
+InputTask inputTask = new();
+inputTask.Start(loggerFactory, memoryPool.Reader, processingPool.Writer);
+
+Console.WriteLine("Running... press any key to stop");
+Console.ReadKey();
+
+processingTask.Stop();
+inputTask.Stop();
\ No newline at end of file
diff --git a/Software/TS.NET/source/TS.NET.Engine/TS.NET.Engine.csproj b/Software/TS.NET/source/TS.NET.Engine/TS.NET.Engine.csproj
new file mode 100644
index 0000000..d5dafc7
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Engine/TS.NET.Engine.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+ True
+ 0.1.0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Software/TS.NET/source/TS.NET.Engine/Tasks/InputTask.cs b/Software/TS.NET/source/TS.NET.Engine/Tasks/InputTask.cs
new file mode 100644
index 0000000..b1222d6
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Engine/Tasks/InputTask.cs
@@ -0,0 +1,79 @@
+using Microsoft.Extensions.Logging;
+using System;
+
+namespace TS.NET.Engine
+{
+ // The job of this task is to read from the thunderscope as fast as possible with minimal jitter
+ internal class InputTask
+ {
+ private CancellationTokenSource? cancelTokenSource;
+ private Task? taskLoop;
+
+ public void Start(ILoggerFactory loggerFactory, BlockingChannelReader memoryPool, BlockingChannelWriter processingPool)
+ {
+ var logger = loggerFactory.CreateLogger("InputTask");
+ cancelTokenSource = new CancellationTokenSource();
+ taskLoop = Task.Factory.StartNew(() => Loop(logger, memoryPool, processingPool, cancelTokenSource.Token), TaskCreationOptions.LongRunning);
+ }
+
+ public void Stop()
+ {
+ cancelTokenSource?.Cancel();
+ taskLoop?.Wait();
+ }
+
+ private static void Loop(ILogger logger, BlockingChannelReader memoryPool, BlockingChannelWriter processingPool, CancellationToken cancelToken)
+ {
+ try
+ {
+ Thread.CurrentThread.Name = "TS.NET Input";
+ Thread.CurrentThread.Priority = ThreadPriority.Highest;
+
+ var devices = Thunderscope.IterateDevices();
+ if (devices.Count == 0)
+ throw new Exception("No thunderscopes found");
+ Thunderscope thunderscope = new Thunderscope();
+ thunderscope.Open(devices[0]);
+ thunderscope.EnableChannel(0);
+ thunderscope.EnableChannel(1);
+ thunderscope.EnableChannel(2);
+ thunderscope.EnableChannel(3);
+ thunderscope.Start();
+
+ while (true)
+ {
+ cancelToken.ThrowIfCancellationRequested();
+ var memory = memoryPool.Read();
+ try
+ {
+ thunderscope.Read(memory);
+ }
+ catch (Exception ex)
+ {
+ if (ex.Message == "ReadFile - failed (1359)")
+ {
+ logger.LogError(ex, $"{nameof(InputTask)} error");
+ continue;
+ }
+ throw;
+ }
+ processingPool.Write(memory);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ logger.LogDebug($"{nameof(InputTask)} stopping");
+ throw;
+ }
+ catch (Exception ex)
+ {
+ logger.LogCritical(ex, $"{nameof(InputTask)} error");
+ throw;
+ }
+ finally
+ {
+ logger.LogDebug($"{nameof(InputTask)} stopped");
+ }
+ }
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.Engine/Tasks/ProcessingTask.cs b/Software/TS.NET/source/TS.NET.Engine/Tasks/ProcessingTask.cs
new file mode 100644
index 0000000..fef697f
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Engine/Tasks/ProcessingTask.cs
@@ -0,0 +1,225 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Diagnostics;
+using System.Runtime.Intrinsics.X86;
+
+namespace TS.NET.Engine
+{
+ public class ProcessingTask
+ {
+ private CancellationTokenSource? cancelTokenSource;
+ private Task? taskLoop;
+
+ //, Action> action
+ public void Start(ILoggerFactory loggerFactory, BlockingChannelReader processingPool, BlockingChannelWriter memoryPool)
+ {
+ var logger = loggerFactory.CreateLogger("ProcessingTask");
+ cancelTokenSource = new CancellationTokenSource();
+ ulong capacityBytes = 4 * 100 * 1000 * 1000; // Maximum capacity = 100M samples per channel
+ // Bridge is cross-process shared memory for the UI to read triggered acquisitions
+ // The trigger point is _always_ in the middle of the channel block, and when the UI sets positive/negative trigger point, it's just moving the UI viewport
+ ThunderscopeBridgeWriter bridge = new(new ThunderscopeBridgeOptions("ThunderScope.1", capacityBytes), loggerFactory);
+ taskLoop = Task.Factory.StartNew(() => Loop(logger, processingPool, memoryPool, bridge, cancelTokenSource.Token), TaskCreationOptions.LongRunning);
+ }
+
+ public void Stop()
+ {
+ cancelTokenSource?.Cancel();
+ taskLoop?.Wait();
+ }
+
+ // The job of this task - pull data from scope driver/simulator, shuffle if 2/4 channels, horizontal sum, trigger, and produce window segments.
+ private static void Loop(ILogger logger, BlockingChannelReader processingPool, BlockingChannelWriter memoryPool, ThunderscopeBridgeWriter bridge, CancellationToken cancelToken)
+ {
+ try
+ {
+ Thread.CurrentThread.Name = "TS.NET Processing";
+
+ // Configuration values to be updated during runtime... conveiniently all on ThunderscopeMemoryBridgeHeader
+ ThunderscopeConfiguration config = new()
+ {
+ Channels = Channels.Four,
+ ChannelLength = 10 * 1000000,//(ulong)ChannelLength.OneHundredM,
+ HorizontalSumLength = HorizontalSumLength.None,
+ TriggerChannel = TriggerChannel.One,
+ TriggerMode = TriggerMode.Normal
+ };
+ bridge.Configuration = config;
+
+ ThunderscopeMonitoring monitoring = new()
+ {
+ TotalAcquisitions = 0,
+ MissedAcquisitions = 0
+ };
+ bridge.Monitoring = monitoring;
+ var bridgeWriterSemaphore = bridge.GetWriterSemaphore();
+
+ // Various buffers allocated once and reused forevermore.
+ //Memory hardwareBuffer = new byte[ThunderscopeMemory.Length];
+ // Shuffle buffers. Only needed for 2/4 channel modes.
+ Span shuffleBuffer = new byte[ThunderscopeMemory.Length];
+ // --2 channel buffers
+ int blockLength_2 = (int)ThunderscopeMemory.Length / 2;
+ Span postShuffleCh1_2 = shuffleBuffer.Slice(0, blockLength_2);
+ Span postShuffleCh2_2 = shuffleBuffer.Slice(blockLength_2, blockLength_2);
+ // --4 channel buffers
+ int blockLength_4 = (int)ThunderscopeMemory.Length / 4;
+ Span postShuffleCh1_4 = shuffleBuffer.Slice(0, blockLength_4);
+ Span postShuffleCh2_4 = shuffleBuffer.Slice(blockLength_4, blockLength_4);
+ Span postShuffleCh3_4 = shuffleBuffer.Slice(blockLength_4 * 2, blockLength_4);
+ Span postShuffleCh4_4 = shuffleBuffer.Slice(blockLength_4 * 3, blockLength_4);
+
+ Span triggerIndices = new uint[ThunderscopeMemory.Length / 1000]; // 1000 samples is the minimum holdoff
+ Span holdoffEndIndices = new uint[ThunderscopeMemory.Length / 1000]; // 1000 samples is the minimum holdoff
+ RisingEdgeTriggerAlt trigger = new(200, 190, (ulong)(config.ChannelLength/2));
+
+ DateTimeOffset startTime = DateTimeOffset.UtcNow;
+ uint dequeueCounter = 0;
+ uint oneSecondHoldoffCount = 0;
+ // HorizontalSumUtility.ToDivisor(horizontalSumLength)
+ Stopwatch oneSecond = Stopwatch.StartNew();
+
+ var circularBuffer1 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length);
+ var circularBuffer2 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length);
+ var circularBuffer3 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length);
+ var circularBuffer4 = new ChannelCircularAlignedBuffer((uint)config.ChannelLength + ThunderscopeMemory.Length);
+
+ while (true)
+ {
+ cancelToken.ThrowIfCancellationRequested();
+ var memory = processingPool.Read(cancelToken);
+ // Add a zero-wait mechanism here that allows for configuration values to be updated
+ // (which will require updating many of the intermediate variables/buffers)
+ dequeueCounter++;
+ int channelLength = config.ChannelLength;
+ switch (config.Channels)
+ {
+ // Processing pipeline:
+ // Shuffle (if needed)
+ // Horizontal sum (EDIT: triggering should happen _before_ horizontal sum)
+ // Write to circular buffer
+ // Trigger
+ // Data segment on trigger (if needed)
+ case Channels.None:
+ break;
+ case Channels.One:
+ // Horizontal sum (EDIT: triggering should happen _before_ horizontal sum)
+ //if (config.HorizontalSumLength != HorizontalSumLength.None)
+ // throw new NotImplementedException();
+ // Write to circular buffer
+ circularBuffer1.Write(memory.Span);
+ // Trigger
+ if (config.TriggerChannel != TriggerChannel.None)
+ {
+ var triggerChannelBuffer = config.TriggerChannel switch
+ {
+ TriggerChannel.One => memory.Span,
+ _ => throw new ArgumentException("Invalid TriggerChannel value")
+ };
+ trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount);
+ }
+ // Finished with the memory, return it
+ memoryPool.Write(memory);
+ break;
+ case Channels.Two:
+ // Shuffle
+ Shuffle.TwoChannels(input: memory.Span, output: shuffleBuffer);
+ // Finished with the memory, return it
+ memoryPool.Write(memory);
+ // Horizontal sum (EDIT: triggering should happen _before_ horizontal sum)
+ //if (config.HorizontalSumLength != HorizontalSumLength.None)
+ // throw new NotImplementedException();
+ // Write to circular buffer
+ circularBuffer1.Write(postShuffleCh1_2);
+ circularBuffer2.Write(postShuffleCh2_2);
+ // Trigger
+ if (config.TriggerChannel != TriggerChannel.None)
+ {
+ var triggerChannelBuffer = config.TriggerChannel switch
+ {
+ TriggerChannel.One => postShuffleCh1_2,
+ TriggerChannel.Two => postShuffleCh2_2,
+ _ => throw new ArgumentException("Invalid TriggerChannel value")
+ };
+ trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount);
+ }
+ break;
+ case Channels.Four:
+ // Shuffle
+ Shuffle.FourChannels(input: memory.Span, output: shuffleBuffer);
+ // Finished with the memory, return it
+ memoryPool.Write(memory);
+ // Horizontal sum (EDIT: triggering should happen _before_ horizontal sum)
+ //if (config.HorizontalSumLength != HorizontalSumLength.None)
+ // throw new NotImplementedException();
+ // Write to circular buffer
+ circularBuffer1.Write(postShuffleCh1_4);
+ circularBuffer2.Write(postShuffleCh2_4);
+ circularBuffer3.Write(postShuffleCh3_4);
+ circularBuffer4.Write(postShuffleCh4_4);
+ // Trigger
+ if (config.TriggerChannel != TriggerChannel.None)
+ {
+ var triggerChannelBuffer = config.TriggerChannel switch
+ {
+ TriggerChannel.One => postShuffleCh1_4,
+ TriggerChannel.Two => postShuffleCh2_4,
+ TriggerChannel.Three => postShuffleCh3_4,
+ TriggerChannel.Four => postShuffleCh4_4,
+ _ => throw new ArgumentException("Invalid TriggerChannel value")
+ };
+ trigger.ProcessSimd(input: triggerChannelBuffer, triggerIndices: triggerIndices, out uint triggerCount, holdoffEndIndices: holdoffEndIndices, out uint holdoffEndCount);
+ monitoring.TotalAcquisitions += holdoffEndCount;
+ oneSecondHoldoffCount += holdoffEndCount;
+ if (holdoffEndCount > 0)
+ {
+ for (int i = 0; i < holdoffEndCount; i++)
+ {
+ if (bridge.IsReadyToWrite)
+ {
+ bridge.Monitoring = monitoring;
+ var bridgeSpan = bridge.Span;
+ uint holdoffEndIndex = (uint)postShuffleCh1_4.Length - holdoffEndIndices[i];
+ circularBuffer1.Read(bridgeSpan.Slice(0, channelLength), holdoffEndIndex);
+ circularBuffer2.Read(bridgeSpan.Slice(channelLength, channelLength), holdoffEndIndex);
+ circularBuffer3.Read(bridgeSpan.Slice(channelLength + channelLength, channelLength), holdoffEndIndex);
+ circularBuffer4.Read(bridgeSpan.Slice(channelLength + channelLength + channelLength, channelLength), holdoffEndIndex);
+ bridge.DataWritten();
+ bridgeWriterSemaphore.Release(); // Signal to the reader that data is available
+ }
+ else
+ {
+ monitoring.MissedAcquisitions++;
+ }
+ }
+ }
+ }
+ //logger.LogInformation($"Dequeue #{dequeueCounter++}, Ch1 triggers: {triggerCount1}, Ch2 triggers: {triggerCount2}, Ch3 triggers: {triggerCount3}, Ch4 triggers: {triggerCount4} ");
+ break;
+ }
+
+ if (oneSecond.ElapsedMilliseconds >= 1000)
+ {
+ logger.LogDebug($"Triggers/sec: {oneSecondHoldoffCount / (oneSecond.ElapsedMilliseconds * 0.001):F2}, dequeue count: {dequeueCounter}, trigger count: {monitoring.TotalAcquisitions}, UI displayed triggers: {monitoring.TotalAcquisitions - monitoring.MissedAcquisitions}, UI dropped triggers: {monitoring.MissedAcquisitions}");
+ oneSecond.Restart();
+ oneSecondHoldoffCount = 0;
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ logger.LogDebug($"{nameof(ProcessingTask)} stopping");
+ throw;
+ }
+ catch (Exception ex)
+ {
+ logger.LogCritical(ex, $"{nameof(ProcessingTask)} error");
+ throw;
+ }
+ finally
+ {
+ logger.LogDebug($"{nameof(ProcessingTask)} stopped");
+ }
+ }
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.Simulator/Program.cs b/Software/TS.NET/source/TS.NET.Simulator/Program.cs
new file mode 100644
index 0000000..94da69f
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Simulator/Program.cs
@@ -0,0 +1,53 @@
+using Cloudtoid.Interprocess;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using TS.NET;
+
+Console.Title = "Simulator";
+using (Process p = Process.GetCurrentProcess())
+ p.PriorityClass = ProcessPriorityClass.High;
+
+int samplingRate = 1000000000;
+int byteBufferSize = 8000000;
+int frequency = 1000000;
+int samplesForOneCycle = samplingRate / frequency;
+
+// Configure interprocess comms
+using var loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(options => { options.SingleLine = true; options.TimestampFormat = "HH:mm:ss "; }).AddFilter(level => level >= LogLevel.Debug));
+var logger = loggerFactory.CreateLogger("Simulator");
+var factory = new QueueFactory(loggerFactory);
+var options = new QueueOptions(queueName: "ThunderScope", bytesCapacity: 4 * byteBufferSize);
+using var publisher = factory.CreatePublisher(options);
+
+Memory waveformBytes = new byte[byteBufferSize];
+//Waveforms.FourChannelSine(sineBytes.Span, samplingRate, frequency);
+Waveforms.FourChannelCount(waveformBytes.Span);
+
+// Transmit messages
+ulong counter = 0;
+ulong previousCount = 0;
+var startTimestamp = DateTime.UtcNow;
+int totalTime = 0;
+Stopwatch oneSecond = Stopwatch.StartNew();
+while (true)
+{
+ if (publisher.TryEnqueue(waveformBytes.Span))
+ {
+ counter++;
+ //logger.LogInformation($"Enqueue #{counter}");
+ }
+ totalTime += 8;
+
+ if (oneSecond.ElapsedMilliseconds >= 1000)
+ {
+ logger.LogDebug($"Counter: {counter}, counts/sec: {counter - previousCount}, samples sent: {counter * 8000000}");
+ previousCount = counter;
+ oneSecond.Restart();
+ }
+
+ var duration = DateTime.UtcNow - startTimestamp;
+ var sleepTime = totalTime - (int)duration.TotalMilliseconds;
+ if (sleepTime < 0)
+ sleepTime = 0;
+ Thread.Sleep(sleepTime);
+}
\ No newline at end of file
diff --git a/Software/TS.NET/source/TS.NET.Simulator/TS.NET.Simulator.csproj b/Software/TS.NET/source/TS.NET.Simulator/TS.NET.Simulator.csproj
new file mode 100644
index 0000000..0a93801
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Simulator/TS.NET.Simulator.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Software/TS.NET/source/TS.NET.Testbench/Program.cs b/Software/TS.NET/source/TS.NET.Testbench/Program.cs
new file mode 100644
index 0000000..d578be9
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Testbench/Program.cs
@@ -0,0 +1,58 @@
+// See https://aka.ms/new-console-template for more information
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using TS.NET;
+
+using (Process p = Process.GetCurrentProcess())
+ p.PriorityClass = ProcessPriorityClass.High;
+
+Console.WriteLine("Waiting for key press...");
+Console.ReadKey();
+
+Span data = new byte[8 * 1000000];
+ChannelCircularAlignedBuffer buffer = new ChannelCircularAlignedBuffer(10 * 1000000);
+
+data.Fill(1);
+buffer.Write(data);
+data.Fill(2);
+buffer.Write(data);
+
+Span readData = new byte[10 * 1000000];
+buffer.Read(readData, 7000000);
+
+return;
+unsafe
+{
+
+ ThunderscopeMemory block = new();
+ Console.WriteLine($"Starting acquisition, block size: {ThunderscopeMemory.Length}");
+
+ var devices = Thunderscope.IterateDevices();
+ if (devices.Count == 0)
+ throw new Exception("No thunderscopes found");
+ Thunderscope ts = new Thunderscope();
+ ts.Open(devices[0]);
+ ts.EnableChannel(0);
+ ts.EnableChannel(1);
+ ts.EnableChannel(2);
+ ts.EnableChannel(3);
+ ts.Start();
+
+ ulong counter = 0;
+ Stopwatch stopwatch = Stopwatch.StartNew();
+ while (!Console.KeyAvailable)
+ {
+ ts.Read(block);
+ counter++;
+ if (stopwatch.ElapsedMilliseconds > 1000)
+ {
+ stopwatch.Restart();
+ Console.WriteLine($"{counter * ThunderscopeMemory.Length}");
+ }
+ }
+
+ Console.WriteLine($"devices count: {devices.Count}");
+ Console.ReadKey();
+}
+
+
diff --git a/Software/TS.NET/source/TS.NET.Testbench/TS.NET.Testbench.csproj b/Software/TS.NET/source/TS.NET.Testbench/TS.NET.Testbench.csproj
new file mode 100644
index 0000000..c3c16fa
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Testbench/TS.NET.Testbench.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+ True
+
+
+
+
+
+
+
diff --git a/Software/TS.NET/source/TS.NET.Tests/RisingEdgeTriggerTests.cs b/Software/TS.NET/source/TS.NET.Tests/RisingEdgeTriggerTests.cs
new file mode 100644
index 0000000..91aabcc
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Tests/RisingEdgeTriggerTests.cs
@@ -0,0 +1,115 @@
+using System;
+using Xunit;
+
+namespace TS.NET.Tests
+{
+ public class RisingEdgeTriggerTests
+ {
+ //[Fact]
+ //public void SituationA_NonSimd()
+ //{
+ // var data = RisingEdgeTriggerSituations.SituationA();
+ // RisingEdgeTrigger trigger = new(data.TriggerLevel, data.ArmLevel, data.HoldoffSamples);
+ // Span actualTriggers = new ulong[data.ExpectedTriggers.Length];
+ // trigger.Process(data.Input.Span, actualTriggers);
+
+ // for (int i = 0; i < actualTriggers.Length; i++)
+ // {
+ // Assert.Equal(data.ExpectedTriggers.Span[i], actualTriggers[i]);
+ // }
+ //}
+
+ //[Fact]
+ //public void SituationA_Simd()
+ //{
+ // var data = RisingEdgeTriggerSituations.SituationA();
+ // RisingEdgeTrigger trigger = new(data.TriggerLevel, data.ArmLevel, data.HoldoffSamples);
+ // Span actualTriggers = new ulong[data.ExpectedTriggers.Length];
+ // trigger.ProcessSimd(data.Input.Span, actualTriggers);
+
+ // for (int i = 0; i < actualTriggers.Length; i++)
+ // {
+ // Assert.Equal(data.ExpectedTriggers.Span[i], actualTriggers[i]);
+ // }
+ //}
+
+ [Fact]
+ public void FiftySample()
+ {
+ RisingEdgeTriggerAlt trigger = new(130, 120, 10);
+ Span triggerIndices = new uint[10000];
+ Span holdoffEndIndices = new uint[10000];
+ Span input = new byte[50];
+ input.Clear();
+ input[16] = 200;
+ trigger.ProcessSimd(input, triggerIndices, out uint triggerCount, holdoffEndIndices, out uint holdoffEndCount);
+
+ }
+
+ [Fact]
+ public void SituationB_Simd()
+ {
+ var situation = RisingEdgeTriggerSituations.SituationB();
+ RisingEdgeTriggerAlt trigger = new(situation.TriggerLevel, situation.ArmLevel, situation.HoldoffSamples);
+ Span triggerIndices = new uint[10000];
+ Span holdoffEndIndices = new uint[10000];
+
+ for (int i = 0; i < situation.ChunkCount; i++)
+ {
+ trigger.ProcessSimd(
+ situation.Input.Span.Slice((int)(i * situation.ChunkSize), (int)situation.ChunkSize),
+ triggerIndices,
+ out uint triggerCount,
+ holdoffEndIndices,
+ out uint holdoffEndCount);
+ if (triggerCount > 0)
+ Console.WriteLine("Hi");
+ if (holdoffEndCount > 0)
+ Console.WriteLine("Hi");
+
+ if(!situation.ExpectedTriggerIndices[i].IsEmpty)
+ {
+ Assert.Equal(triggerCount, (uint)situation.ExpectedTriggerIndices[i].Length);
+ int n = 0;
+ foreach(var index in situation.ExpectedTriggerIndices[i].Span)
+ {
+ Assert.Equal(index, triggerIndices[n++]);
+ }
+ }
+
+ if (!situation.ExpectedHoldoffEndIndices[i].IsEmpty)
+ {
+ Assert.Equal(holdoffEndCount, (uint)situation.ExpectedHoldoffEndIndices[i].Length);
+ int n = 0;
+ foreach (var index in situation.ExpectedHoldoffEndIndices[i].Span)
+ {
+ Assert.Equal(index, holdoffEndIndices[n++]);
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public void SituationC_Simd()
+ {
+ var situation = RisingEdgeTriggerSituations.SituationC();
+ RisingEdgeTriggerAlt trigger = new(situation.TriggerLevel, situation.ArmLevel, situation.HoldoffSamples);
+ Span triggerIndices = new uint[10000];
+ Span holdoffEndIndices = new uint[10000];
+
+ for (int i = 0; i < situation.ChunkCount; i++)
+ {
+ trigger.ProcessSimd(
+ situation.Input.Span.Slice((int)(i * situation.ChunkSize), (int)situation.ChunkSize),
+ triggerIndices,
+ out uint triggerCount,
+ holdoffEndIndices,
+ out uint holdoffEndCount);
+ if (triggerCount > 0)
+ Console.WriteLine("Hi");
+ if (holdoffEndCount > 0)
+ Console.WriteLine("Hi");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Software/TS.NET/source/TS.NET.Tests/Situations/RisingEdgeTriggerSituations.cs b/Software/TS.NET/source/TS.NET.Tests/Situations/RisingEdgeTriggerSituations.cs
new file mode 100644
index 0000000..95e16f7
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Tests/Situations/RisingEdgeTriggerSituations.cs
@@ -0,0 +1,112 @@
+using System;
+
+namespace TS.NET.Tests
+{
+ public class TriggerSituation
+ {
+ public byte TriggerLevel { get; set; }
+ public byte ArmLevel { get; set; }
+ public uint HoldoffSamples { get; set; }
+
+ public uint ChunkSize { get; set; }
+ public uint ChunkCount { get; set; }
+ public Memory Input { get; set; }
+ public Memory[] ExpectedTriggerIndices { get; set; }
+ public Memory[] ExpectedHoldoffEndIndices { get; set; }
+ }
+
+ public class RisingEdgeTriggerSituations
+ {
+ // Trigger at [0] and [^1]
+ //public static TriggerSituation SituationA()
+ //{
+ // Memory inputMemory = new byte[8000000];
+ // var data = inputMemory.Span;
+
+ // data[0] = 127;
+ // data.Slice(1, 3999999).Fill(255);
+ // data.Slice(4000000, 4000000).Fill(0);
+ // data[7999999] = 127;
+
+ // // TO DO: fix this
+ // Memory triggerIndices = new uint[1];
+ // var result = triggerIndices.Span;
+ // //result[0] = 0x01;
+ // //result[^1] = 0x8000000000000000;
+
+ // return new() { TriggerLevel = 127, ArmLevel = 117, HoldoffSamples = 1000, Input = inputMemory, ExpectedTriggerIndices = triggerIndices };
+ //}
+
+ //4 sample wide 1 hz pulse at sample 0
+ public static TriggerSituation SituationB()
+ {
+ const int chunkCount = 120;
+ TriggerSituation situation = new TriggerSituation()
+ {
+ TriggerLevel = 127,
+ ArmLevel = 117,
+ HoldoffSamples = 50 * 1000000,
+
+ ChunkSize = 8388608,
+ ChunkCount = chunkCount,
+ Input = new byte[8388608 * chunkCount],
+
+ ExpectedTriggerIndices = new Memory[chunkCount],
+ ExpectedHoldoffEndIndices = new Memory[chunkCount],
+ };
+
+ situation.Input.Span.Clear();
+ situation.Input.Span[0] = 255;
+ situation.Input.Span[1] = 255;
+ situation.Input.Span[2] = 255;
+ situation.Input.Span[3] = 255;
+
+ situation.ExpectedTriggerIndices[0] = new uint[1];
+ situation.ExpectedTriggerIndices[0].Span[0] = 0;
+ var quotient = situation.HoldoffSamples / situation.ChunkSize;
+ var remainder = situation.HoldoffSamples % situation.ChunkSize;
+ situation.ExpectedHoldoffEndIndices[quotient] = new uint[1];
+ situation.ExpectedHoldoffEndIndices[quotient].Span[0] = remainder;
+
+ return situation;
+ }
+
+ //4 sample wide 51hz pulse repeated 3 times
+ public static TriggerSituation SituationC()
+ {
+ const int chunkCount = 120;
+ TriggerSituation situation = new TriggerSituation()
+ {
+ TriggerLevel = 127,
+ ArmLevel = 117,
+ HoldoffSamples = 5 * 1000000,
+
+ ChunkSize = 8388608,
+ ChunkCount = chunkCount,
+ Input = new byte[8388608 * chunkCount],
+
+ ExpectedTriggerIndices = new Memory[chunkCount],
+ ExpectedHoldoffEndIndices = new Memory[chunkCount],
+ };
+
+ // Every 4901960, a pulse
+ situation.Input.Span.Clear();
+ for(int i = 0; i < situation.Input.Length; i+= 4901960)
+ {
+ situation.Input.Span[i] = 255;
+ situation.Input.Span[i+1] = 255;
+ situation.Input.Span[i+2] = 255;
+ situation.Input.Span[i+3] = 255;
+ }
+
+ situation.ExpectedTriggerIndices[0] = new uint[1];
+ situation.ExpectedTriggerIndices[0].Span[0] = 0;
+ var quotient = situation.HoldoffSamples / situation.ChunkSize;
+ var remainder = situation.HoldoffSamples % situation.ChunkSize;
+ situation.ExpectedHoldoffEndIndices[quotient] = new uint[1];
+ situation.ExpectedHoldoffEndIndices[quotient].Span[0] = remainder;
+
+ return situation;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Software/TS.NET/source/TS.NET.Tests/TS.NET.Tests.csproj b/Software/TS.NET/source/TS.NET.Tests/TS.NET.Tests.csproj
new file mode 100644
index 0000000..ac97a28
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.Tests/TS.NET.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net6.0
+ enable
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/.gitignore b/Software/TS.NET/source/TS.NET.UI.Avalonia/.gitignore
new file mode 100644
index 0000000..8afdcb6
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/.gitignore
@@ -0,0 +1,454 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+##
+## Visual Studio Code
+##
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/App.axaml b/Software/TS.NET/source/TS.NET.UI.Avalonia/App.axaml
new file mode 100644
index 0000000..bff36cb
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/App.axaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/App.axaml.cs b/Software/TS.NET/source/TS.NET.UI.Avalonia/App.axaml.cs
new file mode 100644
index 0000000..8199e31
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/App.axaml.cs
@@ -0,0 +1,27 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace TS.NET.UI.Avalonia
+{
+ public class App : Application
+ {
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow
+ {
+ DataContext = new MainWindowViewModel(),
+ };
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Channel.axaml b/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Channel.axaml
new file mode 100644
index 0000000..8275d13
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Channel.axaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Channel.axaml.cs b/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Channel.axaml.cs
new file mode 100644
index 0000000..f642b81
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Channel.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace TS.NET.UI.Avalonia.Controls
+{
+ public partial class Channel : UserControl
+ {
+ public Channel()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/ChannelViewModel.cs b/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/ChannelViewModel.cs
new file mode 100644
index 0000000..89984bb
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/ChannelViewModel.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TS.NET.UI.Avalonia.Controls
+{
+ internal class ChannelViewModel
+ {
+ public string Name { get; set; } = "Channel X";
+ public bool Enabled { get; set; } = true;
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Timebase.axaml b/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Timebase.axaml
new file mode 100644
index 0000000..2708bcd
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Timebase.axaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ 100M samples
+ 10M samples
+ 1M samples
+ 100k samples
+ 10k samples
+ 1k samples
+
+
+
+ 100M samples
+ 10M samples
+ 1M samples
+ 100k samples
+ 10k samples
+ 1k samples
+
+
+
+
+
+
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Timebase.axaml.cs b/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Timebase.axaml.cs
new file mode 100644
index 0000000..28f2fce
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/Controls/Timebase.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace TS.NET.UI.Avalonia.Controls
+{
+ public partial class Timebase : UserControl
+ {
+ public Timebase()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/DarkThemeOverrides.xaml b/Software/TS.NET/source/TS.NET.UI.Avalonia/DarkThemeOverrides.xaml
new file mode 100644
index 0000000..aa95b00
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/DarkThemeOverrides.xaml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/MainWindow.axaml b/Software/TS.NET/source/TS.NET.UI.Avalonia/MainWindow.axaml
new file mode 100644
index 0000000..69220ff
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/MainWindow.axaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Waiting for bridge connection...
+
+
+
+
diff --git a/Software/TS.NET/source/TS.NET.UI.Avalonia/MainWindow.axaml.cs b/Software/TS.NET/source/TS.NET.UI.Avalonia/MainWindow.axaml.cs
new file mode 100644
index 0000000..cdfc988
--- /dev/null
+++ b/Software/TS.NET/source/TS.NET.UI.Avalonia/MainWindow.axaml.cs
@@ -0,0 +1,209 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
+using Cloudtoid.Interprocess;
+using FluentAvalonia.Styling;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using ScottPlot;
+using ScottPlot.Avalonia;
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace TS.NET.UI.Avalonia
+{
+ public partial class MainWindow : Window
+ {
+ private AvaPlot avaPlot1;
+ private Label lblStatus;
+ private NumericUpDown upDownIndex;
+ private TextBlock textBlockInfo;
+ private double[] channel1 = null;
+ private double[] channel2 = null;
+ private double[] channel3 = null;
+ private double[] channel4 = null;
+ private ScottPlot.Plottable.HLine triggerLine;
+ private CancellationTokenSource cancellationTokenSource;
+ private Task displayTask;
+ //private IPublisher forwarderInput;
+ //private Memory forwarderInputBuffer = new byte[10000];
+ private ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
+
+ public MainWindow()
+ {
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+
+ //var faTheme = AvaloniaLocator.Current.GetService();
+ //faTheme.RequestedTheme = "Dark";
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ channel1 = ArrayPool.Shared.Rent(1);
+ channel2 = ArrayPool.Shared.Rent(1);
+ channel3 = ArrayPool.Shared.Rent(1);
+ channel4 = ArrayPool.Shared.Rent(1);
+
+ avaPlot1 = this.Find("AvaPlot1");
+ lblStatus = this.Find