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 @@ + + + + + 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