A ProEssentials v11 WinUI 3 .NET 10 demonstration of the highest-performance
real-time line charting pattern using PesgoWinUI — combining local-memory
UseDataAtLocation, CircularBuffers, GPU ComputeShader with
StagingBuffers, and Filter2D3D data reduction to sustain a continuous
8 million float real-time update at 15ms intervals.
Extends the base example 146 pattern with a custom right-click Zoom Mode menu that lets you switch live between two zooming behaviors — demonstrating the single-line timer tick difference between examples 145 and 146.
The chart-configuration code is identical to the WPF and WinForms versions of this example. The ProEssentials API does not change between interfaces; only the window plumbing differs.
4 subsets × 2,000,000 points = 8,000,000 floats updated continuously at 150 new samples per subset every 15ms. The chart never allocates new memory, never shifts existing data, and the GPU never waits on a CPU copy — achieving the maximum throughput possible in a .NET WinUI application.
This repo combines examples 145 and 146 into a single app, toggling between their zoom behaviors via the custom right-click menu. The subtitle updates to reflect the active mode.
Zoom in → zoom window stays fixed
New data streams past behind it
When zoomed in, the zoom window does not advance. New data continuously appends to the ring buffer and updates the full axis extents, but the zoomed viewport is anchored in time. Use this to study a feature in detail while the live stream keeps running.
Zoom in → zoom window follows the latest samples
When zoomed in, Zoom.MaxX and Zoom.MinX are each advanced by
nNewSamplesPerTick every frame. The viewport "follows" the data stream,
always showing the most recent 150 samples per tick.
// In Timer_Tick — the only difference between 145 and 146:
if (_zoomScrolls && Pesgo1.PeGrid.Zoom.Mode) // Scrolling mode (145)
{
Pesgo1.PeGrid.Zoom.MaxX += nNew;
Pesgo1.PeGrid.Zoom.MinX += nNew;
}
// Comment out the block above = Stationary mode (146)// App owns these arrays — chart holds a pointer, never copies them
private readonly float[] _xData = new float[2_000_000];
private readonly float[] _yData = new float[8_000_000];
// Give the chart direct access — no transfer, no allocation
Pesgo1.PeData.X.UseDataAtLocation(_xData, 2_000_000);
Pesgo1.PeData.Y.UseDataAtLocation(_yData, 8_000_000);Compare to example 145 which uses FastCopyFrom — that copies 8M floats
into chart-internal circular buffers at init time. UseDataAtLocation is
faster because the GPU staging buffers read from your memory directly.
No copy ever happens between app memory and chart memory.
Pesgo1.PeData.CircularBuffers = true; // set before UseDataAtLocationAppendData writes into a ring buffer rather than shifting all existing data
left. With 2M points per subset, shifting without CircularBuffers would move
8M floats every 15ms. With it, append is O(1) — only the new samples are
written. ComputeShader rendering reads the ring in-order with no penalty.
Pesgo1.PeData.DuplicateDataX = DuplicateData.PointIncrement;
// ...
Pesgo1.PeData.X.UseDataAtLocation(_xData, 2_000_000); // one array, not fourOne shared X array instead of 4 × 2M = 8M floats of redundant X data. The
chart uses _xData[p] for all subsets automatically. AppendData on X sends
only 150 values per tick instead of 600.
Pesgo1.PeConfigure.RenderEngine = RenderEngine.Direct3D;
Pesgo1.PeData.ComputeShader = true; // geometry built on GPU
Pesgo1.PeData.StagingBufferX = true; // DX CPU→GPU transfer path for X
Pesgo1.PeData.StagingBufferY = true; // DX CPU→GPU transfer path for Y
Pesgo1.PeData.Filter2D3D = true; // lossless pixel-level reduction
Pesgo1.PeSpecial.AutoImageReset = false; // suppress intermediate redrawsAt 2M points per subset, only ~1000 points are visible per pixel-column at
any zoom level. Filter2D3D applies a lossless min/max reduction before GPU
transfer — the GPU only processes the visible data density, not the full 2M.
AutoImageReset = false suppresses the intermediate invalidation path,
preventing tearing at the 15ms update rate.
Pesgo1.PeGrid.Configure.ManualScaleControlY = ManualScaleControl.MinMax;
Pesgo1.PeGrid.Configure.ManualMinY = -110;
Pesgo1.PeGrid.Configure.ManualMaxY = 220;
Pesgo1.PeGrid.Configure.ManualScaleControlX = ManualScaleControl.MinMax;
Pesgo1.PeGrid.Configure.ManualMinX = 0;
Pesgo1.PeGrid.Configure.ManualMaxX = 2_000_000;Without manual scaling the chart scans all 8M floats every tick to find min/max. With manual scaling that scan is skipped entirely — the axis extents are updated manually in the timer tick as the counter advances.
// Define menu items — "|" prefix = separator, "Title|Sub1|Sub2" = submenu
Pesgo1.PeUserInterface.Menu.CustomMenuText[0] = "|";
Pesgo1.PeUserInterface.Menu.CustomMenuText[1] = "Zoom Mode|Stationary|Scrolling";
// Default: Stationary checked
Pesgo1.PeUserInterface.Menu.CustomMenuState[1, 1] = CustomMenuState.Checked;
Pesgo1.PeUserInterface.Menu.CustomMenuState[1, 2] = CustomMenuState.UnChecked;
// Both items placed at bottom of built-in menu
Pesgo1.PeUserInterface.Menu.CustomMenuLocation[0] = CustomMenuLocation.Bottom;
Pesgo1.PeUserInterface.Menu.CustomMenuLocation[1] = CustomMenuLocation.Bottom;// Wire the event:
Pesgo1.PeCustomMenu += Pesgo1_PeCustomMenu;
// Handler — radio-style mutual exclusion:
private void Pesgo1_PeCustomMenu(object sender,
Gigasoft.ProEssentials.EventArg.CustomMenuEventArgs e)
{
if (e.MenuIndex != 1) return;
Pesgo1.PeUserInterface.Menu.CustomMenuState[1, 1] = CustomMenuState.UnChecked;
Pesgo1.PeUserInterface.Menu.CustomMenuState[1, 2] = CustomMenuState.UnChecked;
Pesgo1.PeUserInterface.Menu.CustomMenuState[1, e.SubmenuIndex] = CustomMenuState.Checked;
_zoomScrolls = (e.SubmenuIndex == 2);
}The _yData array is a flat interleaved block, subset-major order:
[0 .. 1999999] subset 0 (red)
[2000000 .. 3999999] subset 1 (green)
[4000000 .. 5999999] subset 2 (yellow)
[6000000 .. 7999999] subset 3 (cyan)
UseDataAtLocation(_yData, 8_000_000) passes the total element count.
AppendData(_newY, 150) appends 150 samples per subset per tick, laid out
the same way: [s0p0..s0p149 | s1p0..s1p149 | s2.. | s3..].
WinUI has no DispatcherTimer. The equivalent is a DispatcherQueueTimer
created off the window's own dispatcher queue — it cannot be a field
initializer, because the queue does not exist until the window is constructed:
_timer = DispatcherQueue.CreateTimer(); // in the constructor
_timer.Interval = TimeSpan.FromMilliseconds(15);
_timer.IsRepeating = true;
_timer.Tick += Timer_Tick;
_timer.Start();
// The Tick signature differs from WPF — the timer is the sender:
private void Timer_Tick(DispatcherQueueTimer sender, object e)Everything inside the tick body is unchanged from the WPF version.
| Input | Action |
|---|---|
| Mouse wheel | Horizontal zoom in / out |
| Right-click | Context menu |
| Right-click → Zoom Mode → Stationary | Zoom window fixed (default) |
| Right-click → Zoom Mode → Scrolling | Zoom window follows data |
- Visual Studio 2026 with the .NET desktop development and Windows application development workloads
- .NET 10 SDK
- Internet connection for NuGet restore
- Discrete GPU recommended (integrated GPU will work but may throttle)
No XAML designer: Visual Studio has no XAML designer for WinUI 3 in any edition, so the chart is declared in markup (
<pe:PesgoWinUI x:Name="Pesgo1" />) rather than dragged from the Toolbox. That is a Windows App SDK limitation, not a ProEssentials one.
Window sizing is code, not XAML: a WinUI
Windowis not aControl— it has noHeight/Width/MinWidth, noLoadedevent and noClosing.SizeAndCenterWindow()sets the size with a Win32MoveWindowcall, the chart is initialized from the chart's ownLoadedevent, andClosingbecomesClosed.
1. Clone this repository
2. Open RealTimeCircularBufferWinUI.sln in Visual Studio 2026
3. Build → Rebuild Solution (NuGet restore is automatic)
4. Press F5
5. Mouse-wheel to zoom in, then right-click → Zoom Mode to switch behaviors
References
ProEssentials.Chart.Net10.WinUI.
Package restore is automatic on build.
The AnyCpu package embeds the native rendering engines inside the assembly
and unpacks the one matching the running process at run time, so there is
no native DLL to copy or deploy alongside your app. It also carries the
control's .pri, whose resources are merged into your app's own .pri at
build time.
The nuget.org package is the evaluation build and draws an "Evaluating ProEssentials" watermark across the chart. A licensed installation replaces the assembly and the watermark goes away — performance and behavior are identical.
WinUI deploys differently than WPF and WinForms:
- A .NET app is not a single exe — ship the entire build or publish output folder, never a hand-picked subset.
- The target machine needs two runtimes: the .NET 10 Desktop Runtime
and the Windows App SDK Runtime. Or publish self-contained with
-p:WindowsAppSDKSelfContained=true. - Your development machine already has both, so it will run from an under-filled folder. Always validate on a clean machine.
- WinUI Quickstart
- GigaPrime2D WinUI — 100 Million Points
- WinUI 3D Realtime Surface — ComputeShader
- WinUI Delaunay 2D Contour — Example 147
- WinUI Heatmap Spectrogram — Example 139
- WPF version of this example
- All Examples — GigasoftInc on GitHub
- Full Evaluation Download
- gigasoft.com
Example code is MIT licensed. ProEssentials requires a commercial license for continued use.
