From 6c0bc8023594762f10ec49c6b33aa113a4f23765 Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:12:49 +0100 Subject: [PATCH 01/20] test(mono): run complex chart acceptance inside visible desktop window --- build/mono/SmokeTests/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/build/mono/SmokeTests/Program.cs b/build/mono/SmokeTests/Program.cs index 6b5ccdeb..0d58b3b0 100644 --- a/build/mono/SmokeTests/Program.cs +++ b/build/mono/SmokeTests/Program.cs @@ -321,6 +321,7 @@ private static void DesktopWorkflow() Same(custom.FindForm(), window, "Custom-function editor must be the displayed editor."); Same(scripting.FindForm(), window, "Scripting editor must be displayed in MainView."); VerifyNestedScriptTree(scripting, directory); + VerifyMonoComplexChartVisible(window, bootstrapper, state); var path = Path.Combine(directory, "visible-functions.tslf"); File.WriteAllText(path, "static real Probe(real x) { return x+42; }"); custom.CustomFunctionsEditor.NewDocument(path); From da10c95272e395c6d48b7c022dcd0fedec0ba48c Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:12:51 +0100 Subject: [PATCH 02/20] test(mono): capture real X11 complex chart after Draw --- .../SmokeTests/ComplexChartUiAcceptance.cs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 build/mono/SmokeTests/ComplexChartUiAcceptance.cs diff --git a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs new file mode 100644 index 00000000..0d000e37 --- /dev/null +++ b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs @@ -0,0 +1,108 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; +using System.Threading; +using System.Windows.Forms; +using Computator.NET.Charting.ComplexCharting; +using Computator.NET.Core.Abstract.Controls; +using Computator.NET.Core.Model; +using Computator.NET.DataTypes; +using Computator.NET.DataTypes.Events; +using Computator.NET.Desktop.Bootstrapping; +using Computator.NET.Desktop.Views; + +internal static partial class Program +{ + // Run inside the *actual* bootstrapped desktop window, not a detached chart + // created for GetImage(). Capture the X11 screen itself, not DrawToBitmap. + private static void VerifyMonoComplexChartVisible(MainView window, + WinFormsBootstrapper bootstrapper, ISharedViewState state) + { + state.CurrentView = ViewName.Charting; + EventAggregator.Instance.Publish(new CalculationsModeChangedEvent(CalculationsMode.Complex)); + var view = bootstrapper.Create(); + var chart = (ComplexChart)view.ComplexChart; + chart.Quality = 25; // Keep interactive acceptance quick on hosted Mono. + Application.DoEvents(); + Console.WriteLine("Complex UI initial: " + DescribeComplexLayout(chart)); + Check(window.Visible && chart.Visible && chart.IsHandleCreated && chart.Width > 200 && chart.Height > 150, + "Complex plot does not have a visible, sized, native X11 control: " + DescribeComplexLayout(chart)); + + using (var blank = ScreenshotComplexControl(chart, "complex-visible-before.png")) + Console.WriteLine("Complex UI before draw colored pixels: " + CountDomainColorPixels(blank)); + + bootstrapper.Create().Text = "z²"; + var draw = Find(window, "addToChartButton") as Button; + Check(draw != null && draw.Visible && draw.Enabled, "Real desktop Draw button is not available."); + draw.PerformClick(); + + // The production chart uses async sampling. Pump the real message loop + // until its background calculation and native invalidation have completed. + var timer = Stopwatch.StartNew(); + Bitmap last = null; + int colored = 0; + while (timer.Elapsed < TimeSpan.FromSeconds(25)) + { + Application.DoEvents(); + if (last != null) last.Dispose(); + last = ScreenshotComplexControl(chart, null); + colored = CountDomainColorPixels(last); + if (colored > 200) break; + Thread.Sleep(60); + } + try + { + last.Save(System.IO.Path.Combine(outputDirectory, "complex-visible-after.png"), ImageFormat.Png); + Console.WriteLine("Complex UI after draw: " + DescribeComplexLayout(chart) + + "; colored pixels: " + colored + "; elapsed: " + timer.Elapsed); + Check(colored > 200, + "z² was not painted in the actual Linux/X11 window despite clicking Draw; " + + "on-screen domain color pixels: " + colored); + } + finally { if (last != null) last.Dispose(); } + + using (var export = chart.GetImage(320, 220)) + AssertImageHasVariation(export, 40); + // Restore the original application mode for existing desktop workflow checks. + EventAggregator.Instance.Publish(new CalculationsModeChangedEvent(CalculationsMode.Real)); + } + + private static string DescribeComplexLayout(ComplexChart chart) + { + if (chart.Parent == null) return "unparented"; + return string.Format("bounds={0} visible={1} handle={2} childIndex={3} siblings=[{4}]", + chart.Bounds, chart.Visible, chart.IsHandleCreated, + chart.Parent.Controls.GetChildIndex(chart), + string.Join("; ", chart.Parent.Controls.Cast().Select(c => + c.GetType().Name + ":" + c.Visible + ":" + c.Bounds).ToArray())); + } + + private static Bitmap ScreenshotComplexControl(Control chart, string name) + { + var rect = chart.RectangleToScreen(chart.ClientRectangle); + Check(rect.Width > 0 && rect.Height > 0, + "Complex chart has no X11 rectangle."); + var bitmap = new Bitmap(rect.Width, rect.Height); + using (var graphics = Graphics.FromImage(bitmap)) + graphics.CopyFromScreen(rect.Location, Point.Empty, rect.Size); + if (name != null) + bitmap.Save(System.IO.Path.Combine(outputDirectory, name), ImageFormat.Png); + return bitmap; + } + + private static int CountDomainColorPixels(Bitmap bitmap) + { + int count = 0; + for (int x = 10; x < bitmap.Width - 10; x += 5) + for (int y = 10; y < bitmap.Height - 10; y += 5) + { + var p = bitmap.GetPixel(x,y); + if (Math.Max(p.R,Math.Max(p.G,p.B)) - Math.Min(p.R,Math.Min(p.G,p.B)) > 45 && + Math.Max(p.R,Math.Max(p.G,p.B)) > 85) + count++; + } + return count; + } +} From d52c4378bcccc2593c81828cc7fd72e785b14cf0 Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:19:41 +0100 Subject: [PATCH 03/20] test(mono): exercise five actual X11 Draw clicks at default quality --- .../SmokeTests/ComplexChartUiAcceptance.cs | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs index 0d000e37..c560d498 100644 --- a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs +++ b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs @@ -3,6 +3,7 @@ using System.Drawing; using System.Drawing.Imaging; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; using Computator.NET.Charting.ComplexCharting; @@ -24,7 +25,7 @@ private static void VerifyMonoComplexChartVisible(MainView window, EventAggregator.Instance.Publish(new CalculationsModeChangedEvent(CalculationsMode.Complex)); var view = bootstrapper.Create(); var chart = (ComplexChart)view.ComplexChart; - chart.Quality = 25; // Keep interactive acceptance quick on hosted Mono. + chart.Quality = 50; // Reproduce the production default sampling quality. Application.DoEvents(); Console.WriteLine("Complex UI initial: " + DescribeComplexLayout(chart)); Check(window.Visible && chart.Visible && chart.IsHandleCreated && chart.Width > 200 && chart.Height > 150, @@ -36,7 +37,11 @@ private static void VerifyMonoComplexChartVisible(MainView window, bootstrapper.Create().Text = "z²"; var draw = Find(window, "addToChartButton") as Button; Check(draw != null && draw.Visible && draw.Enabled, "Real desktop Draw button is not available."); - draw.PerformClick(); + var paintCalls = 0; + chart.Paint += (sender, args) => paintCalls++; + for (var attempt = 0; attempt < 5; attempt++) + ClickActualX11Button(draw); // Real pointer/button events, as in production. + Console.WriteLine("X11 Draw clicks delivered (five attempted), initial chart paints: " + paintCalls); // The production chart uses async sampling. Pump the real message loop // until its background calculation and native invalidation have completed. @@ -56,7 +61,7 @@ private static void VerifyMonoComplexChartVisible(MainView window, { last.Save(System.IO.Path.Combine(outputDirectory, "complex-visible-after.png"), ImageFormat.Png); Console.WriteLine("Complex UI after draw: " + DescribeComplexLayout(chart) + - "; colored pixels: " + colored + "; elapsed: " + timer.Elapsed); + "; colored pixels: " + colored + "; elapsed: " + timer.Elapsed + "; chart paint calls: " + paintCalls); Check(colored > 200, "z² was not painted in the actual Linux/X11 window despite clicking Draw; " + "on-screen domain color pixels: " + colored); @@ -105,4 +110,36 @@ private static int CountDomainColorPixels(Bitmap bitmap) } return count; } + [DllImport("libX11.so.6")] + private static extern IntPtr XOpenDisplay(IntPtr displayName); + [DllImport("libX11.so.6")] + private static extern int XCloseDisplay(IntPtr display); + [DllImport("libX11.so.6")] + private static extern int XSync(IntPtr display, int discard); + [DllImport("libXtst.so.6")] + private static extern int XTestFakeMotionEvent(IntPtr display, int screen, int x, int y, ulong delay); + [DllImport("libXtst.so.6")] + private static extern int XTestFakeButtonEvent(IntPtr display, uint button, int press, ulong delay); + + private static void ClickActualX11Button(Control button) + { + var center = button.PointToScreen(new Point(button.Width / 2, button.Height / 2)); + var display = XOpenDisplay(IntPtr.Zero); + Check(display != IntPtr.Zero, "Cannot open native X11 display for Draw mouse input."); + try + { + Check(XTestFakeMotionEvent(display, 0, center.X, center.Y, 0) != 0, + "Native mouse motion to Draw button failed."); + XSync(display, 0); + Application.DoEvents(); + Check(XTestFakeButtonEvent(display, 1, 1, 0) != 0, "X11 Draw mouse press failed."); + XSync(display, 0); + Application.DoEvents(); + Check(XTestFakeButtonEvent(display, 1, 0, 0) != 0, "X11 Draw mouse release failed."); + XSync(display, 0); + Application.DoEvents(); + } + finally { XCloseDisplay(display); } + } + } From 0a66a06b021e88006ceeb7fc8879363657ab14bb Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:19:44 +0100 Subject: [PATCH 04/20] ci(mono): install XTEST native mouse dependency for visible chart checks --- .github/workflows/mono-desktop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mono-desktop.yml b/.github/workflows/mono-desktop.yml index 4e92b47b..0e5215ae 100644 --- a/.github/workflows/mono-desktop.yml +++ b/.github/workflows/mono-desktop.yml @@ -23,7 +23,7 @@ jobs: - name: Install the actual Mono and X11 runtime run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends mono-devel xvfb xauth libgdiplus libgsl-dev fonts-dejavu-core + sudo apt-get install -y --no-install-recommends mono-devel xvfb xauth libxtst6 libgdiplus libgsl-dev fonts-dejavu-core - name: Build the opt-in Framework graph and run desktop acceptance run: ./build.sh --target=Mono-Validate - name: Retain runtime evidence even on failure From e5c30ba32fb6334b846c7504f29dd356070ef75b Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:22:11 +0100 Subject: [PATCH 05/20] test(mono): verify production executable paints complex chart on real X11 screen --- build/mono/StartupProbe/Program.cs | 81 ++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/build/mono/StartupProbe/Program.cs b/build/mono/StartupProbe/Program.cs index 0f21d59e..155e07d0 100644 --- a/build/mono/StartupProbe/Program.cs +++ b/build/mono/StartupProbe/Program.cs @@ -49,6 +49,8 @@ public int Run(string applicationPath, string outputDirectory) // Reflection only provides a deterministic close after the real window paints. var watch = Stopwatch.StartNew(); var shown = false; + var complexRequested = false; + double? complexStartedAt = null; double? firstVisibleAt = null; Exception failure = null; using (var timer = new Timer { Interval = 200 }) @@ -75,6 +77,26 @@ public int Run(string applicationPath, string outputDirectory) return; } if (watch.Elapsed.TotalSeconds - firstVisibleAt.Value < 1) return; + if (complexRequested) + { + var chart = Descendants(form).Single(control => control.GetType().Name == "ComplexChart"); + using (var visible = CaptureX11Pixels(chart)) + { + var colorPixels = CountDomainColorPixels(visible); + if (colorPixels > 200 || watch.Elapsed.TotalSeconds - complexStartedAt.Value > 30) + { + visible.Save(Path.Combine(outputDirectory, "production-complex-after.png"), ImageFormat.Png); + Console.WriteLine("Production complex chart: visible=" + chart.Visible + + " bounds=" + chart.Bounds + " colored-pixels=" + colorPixels); + if (colorPixels <= 200) + throw new InvalidOperationException("Production executable did not paint z² after its real Draw action."); + shown = true; + timer.Stop(); + form.Close(); + } + } + return; + } AssertUsableLayout(form); foreach (var name in new[] { "Computator.NET.Desktop", "Computator.NET.Core", "Computator.NET.Charting", "Computator.NET.Engine" }) { @@ -96,9 +118,9 @@ public int Run(string applicationPath, string outputDirectory) throw new InvalidOperationException("The real application window did not paint its control tree (" + colors.Count + " sampled colors)."); bitmap.Save(Path.Combine(outputDirectory, "startup.png"), ImageFormat.Png); } - shown = true; - timer.Stop(); - form.Close(); + BeginProductionComplexPlot(form, outputDirectory); + complexRequested = true; + complexStartedAt = watch.Elapsed.TotalSeconds; } catch (Exception error) { @@ -125,6 +147,59 @@ public int Run(string applicationPath, string outputDirectory) } } + + private static void BeginProductionComplexPlot(Form form, string outputDirectory) + { + // Exercise mode selection and Draw in the *production* executable's + // Application.Run loop, including its real injected presenters and views. + var menuField = form.GetType().GetField("fdsfdsToolStripMenuItem", + BindingFlags.NonPublic | BindingFlags.Instance); + if (menuField == null) + throw new InvalidOperationException("Missing production complex-mode menu."); + var mode = menuField.GetValue(form) as ToolStripMenuItem; + if (mode == null) + throw new InvalidOperationException("Complex-mode menu has unexpected type."); + mode.PerformClick(); + var children = Descendants(form).ToArray(); + var chart = children.Single(control => control.GetType().Name == "ComplexChart"); + var expression = children.Single(control => control.GetType().Name == "ExpressionTextBox"); + var draw = children.Single(control => control.Name == "addToChartButton") as Button; + if (draw == null || !draw.Visible || !chart.Visible || !chart.IsHandleCreated || + chart.ClientSize.Width < 200 || chart.ClientSize.Height < 150) + throw new InvalidOperationException("Production complex chart or Draw is not actually visible: " + + "chart=" + chart.Bounds + " visible=" + chart.Visible + " draw=" + draw?.Visible); + using (var before = CaptureX11Pixels(chart)) + before.Save(Path.Combine(outputDirectory, "production-complex-before.png"), ImageFormat.Png); + expression.Text = "z²"; + draw.PerformClick(); + Console.WriteLine("Production executable complex UI Draw was clicked; bounds=" + chart.Bounds); + } + + private static Bitmap CaptureX11Pixels(Control control) + { + var rectangle = control.RectangleToScreen(control.ClientRectangle); + if (rectangle.Width < 1 || rectangle.Height < 1) + throw new InvalidOperationException("Invalid native screen rectangle for " + control.Name); + var image = new Bitmap(rectangle.Width, rectangle.Height); + using (var graphics = Graphics.FromImage(image)) + graphics.CopyFromScreen(rectangle.Location, Point.Empty, rectangle.Size); + return image; + } + + private static int CountDomainColorPixels(Bitmap bitmap) + { + var pixels = 0; + for (var x = 10; x < bitmap.Width - 10; x += 5) + for (var y = 10; y < bitmap.Height - 10; y += 5) + { + var pixel = bitmap.GetPixel(x, y); + if (Math.Max(pixel.R, Math.Max(pixel.G, pixel.B)) - + Math.Min(pixel.R, Math.Min(pixel.G, pixel.B)) > 45 && + Math.Max(pixel.R, Math.Max(pixel.G, pixel.B)) > 85) + pixels++; + } + return pixels; + } private static System.Collections.Generic.IEnumerable Descendants(Control parent) { foreach (Control child in parent.Controls) From 46bd10dbf8bb34efa015070b42ed38755cf98763 Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:22:14 +0100 Subject: [PATCH 06/20] ci(mono): clear stale production complex screenshots before acceptance --- build/mono/validate.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/mono/validate.sh b/build/mono/validate.sh index b85b35e0..59782194 100644 --- a/build/mono/validate.sh +++ b/build/mono/validate.sh @@ -56,6 +56,9 @@ mono "$validation_temp/LegacyFixture.exe" "$validation_temp/legacy-settings.dat" rm -f -- "$results/mono-results.xml" "$results/startup.png" "$results/desktop-workflow.png" \ "$results/real-chart.png" "$results/complex-chart.png" "$results/unsupported-3d.png" "$results/function-reference.png" \ "$results/calculator-inputs.png" "$results/numerical-inputs.png" +# Do not mistake screenshots from a previous acceptance run for current evidence. +rm -f -- "$results/production-complex-before.png" "$results/production-complex-after.png" \ + "$results/complex-visible-before.png" "$results/complex-visible-after.png" status=0 COMPUTATOR_DATA_DIRECTORY="$validation_temp/startup" \ XDG_CONFIG_HOME="$validation_temp/config" XDG_CACHE_HOME="$validation_temp/cache" \ From ba91dc49a3b4f1a4f8d22a840089b5e2d03c8e1b Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:34:15 +0100 Subject: [PATCH 07/20] fix(mono): make selected complex chart the frontmost native WinForms layer --- Computator.NET.Desktop/Views/ChartingView.cs | 29 +++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Computator.NET.Desktop/Views/ChartingView.cs b/Computator.NET.Desktop/Views/ChartingView.cs index 814b3a52..174ebfdd 100644 --- a/Computator.NET.Desktop/Views/ChartingView.cs +++ b/Computator.NET.Desktop/Views/ChartingView.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Windows.Forms; using Computator.NET.Charting.Chart3D.UI; using Computator.NET.Charting.ComplexCharting; @@ -6,11 +7,17 @@ using Computator.NET.Core.Abstract.Views; using Computator.NET.DataTypes; using Computator.NET.DataTypes.Charts; +#if __MonoCS__ +using Computator.NET.DataTypes.Events; +#endif namespace Computator.NET.Desktop.Views { public partial class ChartingView : UserControl, IChartingView { +#if __MonoCS__ + private readonly Action _showMonoChart; +#endif public ChartingView(ChartAreaValuesView chartAreaValuesView, Chart2D chart2D, ComplexChart complexChart, Chart3DControl chart3D) : this() { chartAreaValuesView.Dock = DockStyle.Right; @@ -40,6 +47,26 @@ public ChartingView(ChartAreaValuesView chartAreaValuesView, Chart2D chart2D, Co #endif (Control) chartAreaValuesView, }); +#if __MonoCS__ + // All three chart controls are docked Fill in the same panel. + // Hiding a sibling does not reliably lower its native X11 window + // on every Mono/WinForms backend. Select the active chart's actual + // z-order as well as its Visible flag (set by the presenter). + _showMonoChart = mode => + { + IChart active; + if (!Charts.TryGetValue(mode.CalculationsMode, out active)) + return; + var control = active as Control; + if (control == null || control.IsDisposed) + return; + control.BringToFront(); + control.Invalidate(); + }; + EventAggregator.Instance.Subscribe(_showMonoChart); + Disposed += (sender, args) => + EventAggregator.Instance.Unsubscribe(_showMonoChart); +#endif } private ChartingView() From 7a532b1945c201665870aeae45ca6f4e1f59f80e Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:34:18 +0100 Subject: [PATCH 08/20] test(mono): require selected complex plot to own front X11 z-order --- build/mono/SmokeTests/ComplexChartUiAcceptance.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs index c560d498..21f8af62 100644 --- a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs +++ b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs @@ -28,6 +28,8 @@ private static void VerifyMonoComplexChartVisible(MainView window, chart.Quality = 50; // Reproduce the production default sampling quality. Application.DoEvents(); Console.WriteLine("Complex UI initial: " + DescribeComplexLayout(chart)); + Check(chart.Parent.Controls.GetChildIndex(chart) == 0, + "Active complex chart is underneath another chart window in Mono/X11."); Check(window.Visible && chart.Visible && chart.IsHandleCreated && chart.Width > 200 && chart.Height > 150, "Complex plot does not have a visible, sized, native X11 control: " + DescribeComplexLayout(chart)); From e0e2b790affff47d84db16cbcac614314b80f12c Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:36:00 +0100 Subject: [PATCH 09/20] test(mono): cover visible redraw expression resize modes clear and recovery --- .../SmokeTests/ComplexChartUiAcceptance.cs | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs index 21f8af62..d8b2ec5a 100644 --- a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs +++ b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs @@ -19,7 +19,7 @@ internal static partial class Program // Run inside the *actual* bootstrapped desktop window, not a detached chart // created for GetImage(). Capture the X11 screen itself, not DrawToBitmap. private static void VerifyMonoComplexChartVisible(MainView window, - WinFormsBootstrapper bootstrapper, ISharedViewState state) + WinFormsBootstrapper bootstrapper, ISharedViewState state, RecordingErrors errors) { state.CurrentView = ViewName.Charting; EventAggregator.Instance.Publish(new CalculationsModeChangedEvent(CalculationsMode.Complex)); @@ -72,10 +72,130 @@ private static void VerifyMonoComplexChartVisible(MainView window, using (var export = chart.GetImage(320, 220)) AssertImageHasVariation(export, 40); + + // Switch expression and require the live native screen to change, + // rather than merely inspecting GetImage() again. + using (var zSquared = ScreenshotComplexControl(chart, null)) + { + bootstrapper.Create().Text = "z³"; + ClickActualX11Button(draw); + using (var zCubed = WaitForComplexPixels(chart, "complex-visible-z-cubed.png", + image => DifferentComplexPixels(zSquared, image) > 600, 25)) + Check(CountDomainColorPixels(zCubed) > 200, "Changing expression hid the complex domain colors."); + } + + // Real -> 3D -> Complex must make the selected native child frontmost, + // including the separate Mono 3D implementation introduced by #296. + foreach (var mode in new[] { CalculationsMode.Real, CalculationsMode.Fxy, CalculationsMode.Complex }) + { + EventAggregator.Instance.Publish(new CalculationsModeChangedEvent(mode)); + Application.DoEvents(); + Check(chart.Visible == (mode == CalculationsMode.Complex), + "Complex chart visibility did not follow calculation mode " + mode); + if (mode == CalculationsMode.Complex) + Check(chart.Parent.Controls.GetChildIndex(chart) == 0, + "Switching back to Complex left it behind an X11 sibling."); + } + chart.Refresh(); + Application.DoEvents(); + using (var returning = ScreenshotComplexControl(chart, null)) + Check(CountDomainColorPixels(returning) > 200, "Complex chart disappeared after mode switching."); + + var normalSize = window.ClientSize; + try + { + window.ClientSize = new Size(normalSize.Width - 100, normalSize.Height - 75); + Application.DoEvents(); + Check(chart.Width > 200 && chart.Height > 150 && draw.Visible, + "Resizing clipped the active complex plot or its Draw button."); + using (var resized = WaitForComplexPixels(chart, "complex-visible-resized.png", + image => CountDomainColorPixels(image) > 200, 15)) + AssertImageHasVariation(resized, 50); + } + finally { window.ClientSize = normalSize; Application.DoEvents(); } + + var clear = Find(window, "clearChartButton") as Button; + Check(clear != null && clear.Visible, "Desktop Clear button is not available."); + clear.PerformClick(); + Application.DoEvents(); + using (var cleared = ScreenshotComplexControl(chart, "complex-visible-cleared.png")) + Check(CountDomainColorPixels(cleared) == 0, "Clear left stale colors on the visible chart."); + + // A compilation error must be reported through the normal error handler, + // must not repaint old colors, and must not prevent a later valid Draw. + var errorsBefore = errors.Messages.Count; + bootstrapper.Create().Text = "MissingMonoFunction(z)"; + ClickActualX11Button(draw); + Check(errors.Messages.Count > errorsBefore, "Invalid complex expression was not reported."); + using (var invalid = ScreenshotComplexControl(chart, null)) + Check(CountDomainColorPixels(invalid) == 0, "Invalid Draw repainted stale domain colors."); + errors.Messages.Clear(); + + // Clear while an asynchronous drawing is being scheduled: obsolete + // samples must never appear later, including during rapid user input. + bootstrapper.Create().Text = "z⁴"; + ClickActualX11Button(draw); + clear.PerformClick(); + PumpComplexUiFor(TimeSpan.FromMilliseconds(600)); + using (var stale = ScreenshotComplexControl(chart, null)) + Check(CountDomainColorPixels(stale) == 0, "An obsolete async render repainted after Clear."); + + bootstrapper.Create().Text = "z²"; + ClickActualX11Button(draw); + using (var recovered = WaitForComplexPixels(chart, "complex-visible-recovered.png", + image => CountDomainColorPixels(image) > 200, 25)) + AssertImageHasVariation(recovered, 50); // Restore the original application mode for existing desktop workflow checks. EventAggregator.Instance.Publish(new CalculationsModeChangedEvent(CalculationsMode.Real)); } + + private static Bitmap WaitForComplexPixels(Control control, string filename, + Func accepted, int timeoutSeconds) + { + var watch = Stopwatch.StartNew(); + Bitmap latest = null; + while (watch.Elapsed < TimeSpan.FromSeconds(timeoutSeconds)) + { + Application.DoEvents(); + if (latest != null) latest.Dispose(); + latest = ScreenshotComplexControl(control, null); + if (accepted(latest)) break; + Thread.Sleep(75); + } + try + { + latest.Save(System.IO.Path.Combine(outputDirectory, filename), ImageFormat.Png); + Check(accepted(latest), + "Visible X11 complex chart did not reach " + filename + " after " + watch.Elapsed); + var result = latest; + latest = null; + return result; + } + finally { if (latest != null) latest.Dispose(); } + } + + private static int DifferentComplexPixels(Bitmap first, Bitmap second) + { + Check(first.Size == second.Size, "Expression switch unexpectedly resized the chart."); + var changed = 0; + for (var x = 10; x < first.Width - 10; x += 5) + for (var y = 10; y < first.Height - 10; y += 5) + if (first.GetPixel(x, y).ToArgb() != second.GetPixel(x, y).ToArgb()) + changed++; + return changed; + } + + private static void PumpComplexUiFor(TimeSpan duration) + { + var watch = Stopwatch.StartNew(); + while (watch.Elapsed < duration) + { + Application.DoEvents(); + Thread.Sleep(35); + } + } + private static string DescribeComplexLayout(ComplexChart chart) { if (chart.Parent == null) return "unparented"; From 7f8e54db0046b00974351e58fa55710ac3c6c423 Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:36:04 +0100 Subject: [PATCH 10/20] test(mono): pass desktop error recorder into complex chart acceptance --- build/mono/SmokeTests/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/mono/SmokeTests/Program.cs b/build/mono/SmokeTests/Program.cs index 0d58b3b0..228ba16c 100644 --- a/build/mono/SmokeTests/Program.cs +++ b/build/mono/SmokeTests/Program.cs @@ -321,7 +321,7 @@ private static void DesktopWorkflow() Same(custom.FindForm(), window, "Custom-function editor must be the displayed editor."); Same(scripting.FindForm(), window, "Scripting editor must be displayed in MainView."); VerifyNestedScriptTree(scripting, directory); - VerifyMonoComplexChartVisible(window, bootstrapper, state); + VerifyMonoComplexChartVisible(window, bootstrapper, state, errors); var path = Path.Combine(directory, "visible-functions.tslf"); File.WriteAllText(path, "static real Probe(real x) { return x+42; }"); custom.CustomFunctionsEditor.NewDocument(path); From 4f7f164587c0a3f0dc20cbfbb9de033d3524d8a7 Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:39:28 +0100 Subject: [PATCH 11/20] fix(chart): surface async complex render errors without crashing the UI --- .../ComplexCharting/ComplexChart.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Computator.NET.Charting/ComplexCharting/ComplexChart.cs b/Computator.NET.Charting/ComplexCharting/ComplexChart.cs index 534e4a07..df808263 100644 --- a/Computator.NET.Charting/ComplexCharting/ComplexChart.cs +++ b/Computator.NET.Charting/ComplexCharting/ComplexChart.cs @@ -36,6 +36,7 @@ private void OnPropertyChanged(string propertyName) // private Task RedrawTask; private ComplexPoint[,] _pointsValues; private bool _redrawing = false; + public string LastRenderError { get; private set; } private long _redrawVersion; private Function function; private Bitmap image = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); @@ -352,6 +353,7 @@ private void _MouseClick(object s, MouseEventArgs e) public void AddFunction(Function Fz) { + LastRenderError = null; function = Fz; Title = Fz.Name; Redraw(); @@ -451,6 +453,7 @@ private Bitmap ComposeImage(Image source, int width, int height) public void ClearAll() { + LastRenderError = null; ++_redrawVersion; function = null; _pointsValues = null; @@ -498,10 +501,19 @@ public async void Redraw() } catch (Exception) when (function == null || IsDisposed || version != _redrawVersion) { - // A discarded evaluation must not show an error after a new - // plot, clear or close. Current evaluation errors still propagate. + // Superseded evaluations should never report an error. continue; } + catch (Exception exception) + { + // A failed background evaluation must not tear down the + // WinForms UI via this async-void event-path method. + // Keep the control usable and show a visible diagnostic. + Logger.Error(exception, "Complex chart asynchronous redraw failed."); + LastRenderError = exception.Message; + Invalidate(); + break; + } if (function == null || IsDisposed || version != _redrawVersion) { samples.Bitmap.Dispose(); @@ -526,6 +538,15 @@ public async void Redraw() protected override void OnPaint(PaintEventArgs pe) { PaintOn(pe.Graphics, ClientRectangle, image); + if (!string.IsNullOrWhiteSpace(LastRenderError)) + { + var bounds = Rectangle.Inflate(ClientRectangle, -12, -12); + if (bounds.Width > 0 && bounds.Height > 0) + TextRenderer.DrawText(pe.Graphics, "Complex chart rendering failed: " + LastRenderError, + Font, new Rectangle(bounds.Left, bounds.Top, bounds.Width, Math.Min(90, bounds.Height)), + Color.DarkRed, BackColor, + TextFormatFlags.WordBreak | TextFormatFlags.Left | TextFormatFlags.Top); + } } From bd312f731c5dc6fab686dfe607de9afa3213232d Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:42:12 +0100 Subject: [PATCH 12/20] test(mono): verify worker-render exception is visible and later Draw recovers --- .../SmokeTests/ComplexChartUiAcceptance.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs index d8b2ec5a..cf6af87e 100644 --- a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs +++ b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs @@ -3,6 +3,7 @@ using System.Drawing; using System.Drawing.Imaging; using System.Linq; +using System.Numerics; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; @@ -11,6 +12,7 @@ using Computator.NET.Core.Model; using Computator.NET.DataTypes; using Computator.NET.DataTypes.Events; +using Computator.NET.DataTypes.Functions; using Computator.NET.Desktop.Bootstrapping; using Computator.NET.Desktop.Views; @@ -145,6 +147,41 @@ private static void VerifyMonoComplexChartVisible(MainView window, using (var recovered = WaitForComplexPixels(chart, "complex-visible-recovered.png", image => CountDomainColorPixels(image) > 200, 25)) AssertImageHasVariation(recovered, 50); + + // The plotting delegate itself may throw only on the worker task, + // after the Draw handler returned. Confirm the async-void continuation + // cannot terminate the running desktop or permanently poison redraws. + var failingFunction = new Function( + new Func(z => + { + throw new InvalidOperationException("Intentional complex rendering failure"); + }), + FunctionType.Complex) { Name = "Intentional complex rendering failure" }; + chart.AddFunction(failingFunction); + var failureWatch = Stopwatch.StartNew(); + while (failureWatch.Elapsed < TimeSpan.FromSeconds(10) && + string.IsNullOrWhiteSpace(chart.LastRenderError)) + { + Application.DoEvents(); + Thread.Sleep(40); + } + Check(chart.LastRenderError != null && + chart.LastRenderError.Contains("Intentional complex rendering failure"), + "Asynchronous evaluation did not deliver an observable UI error."); + Application.DoEvents(); + using (var failure = ScreenshotComplexControl(chart, "complex-visible-render-failure.png")) + Check(failure.Width > 200 && failure.Height > 150, + "Failed complex render destroyed the native window."); + + chart.ClearAll(); + bootstrapper.Create().Text = "z²"; + ClickActualX11Button(draw); + using (var repaired = WaitForComplexPixels(chart, "complex-visible-after-render-failure.png", + image => CountDomainColorPixels(image) > 200, 25)) + AssertImageHasVariation(repaired, 50); + Check(string.IsNullOrEmpty(chart.LastRenderError), + "A successful redraw did not clear the earlier asynchronous error."); + // Restore the original application mode for existing desktop workflow checks. EventAggregator.Instance.Publish(new CalculationsModeChangedEvent(CalculationsMode.Real)); } From 20bb91794012a16d3684be7438ad26c0739def5d Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Mon, 26 Oct 2026 13:42:15 +0100 Subject: [PATCH 13/20] ci(mono): remove all stale native complex UI screenshots before validation --- build/mono/validate.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build/mono/validate.sh b/build/mono/validate.sh index 59782194..8bdbd247 100644 --- a/build/mono/validate.sh +++ b/build/mono/validate.sh @@ -58,7 +58,10 @@ rm -f -- "$results/mono-results.xml" "$results/startup.png" "$results/desktop-wo "$results/calculator-inputs.png" "$results/numerical-inputs.png" # Do not mistake screenshots from a previous acceptance run for current evidence. rm -f -- "$results/production-complex-before.png" "$results/production-complex-after.png" \ - "$results/complex-visible-before.png" "$results/complex-visible-after.png" + "$results/complex-visible-before.png" "$results/complex-visible-after.png" \ + "$results/complex-visible-z-cubed.png" "$results/complex-visible-resized.png" \ + "$results/complex-visible-cleared.png" "$results/complex-visible-recovered.png" \ + "$results/complex-visible-render-failure.png" "$results/complex-visible-after-render-failure.png" status=0 COMPUTATOR_DATA_DIRECTORY="$validation_temp/startup" \ XDG_CONFIG_HOME="$validation_temp/config" XDG_CACHE_HOME="$validation_temp/cache" \ From 825c5b7d4f3baf7f37d57351c8d3a26d18a4a9bc Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Tue, 27 Oct 2026 00:24:31 +0100 Subject: [PATCH 14/20] fix(mono): include ComplexChartUiAcceptance.cs in developer solution inventory --- Computator.NET.slnx | 1 + 1 file changed, 1 insertion(+) diff --git a/Computator.NET.slnx b/Computator.NET.slnx index 77c6a057..23bb9e28 100644 --- a/Computator.NET.slnx +++ b/Computator.NET.slnx @@ -127,6 +127,7 @@ + From cf82022019758e43513a103152917f28299af1d4 Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Tue, 27 Oct 2026 00:28:18 +0100 Subject: [PATCH 15/20] test(mono): exercise production complex Draw with native X11 click and render diagnostics --- build/mono/StartupProbe/Program.cs | 62 ++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/build/mono/StartupProbe/Program.cs b/build/mono/StartupProbe/Program.cs index 155e07d0..ffcdfddc 100644 --- a/build/mono/StartupProbe/Program.cs +++ b/build/mono/StartupProbe/Program.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Windows.Forms; internal static class Program @@ -89,7 +90,8 @@ public int Run(string applicationPath, string outputDirectory) Console.WriteLine("Production complex chart: visible=" + chart.Visible + " bounds=" + chart.Bounds + " colored-pixels=" + colorPixels); if (colorPixels <= 200) - throw new InvalidOperationException("Production executable did not paint z² after its real Draw action."); + throw new InvalidOperationException("Production executable did not paint z² after its real X11 Draw action; " + + DescribeComplexFunctionState(chart)); shown = true; timer.Stop(); form.Close(); @@ -118,9 +120,10 @@ public int Run(string applicationPath, string outputDirectory) throw new InvalidOperationException("The real application window did not paint its control tree (" + colors.Count + " sampled colors)."); bitmap.Save(Path.Combine(outputDirectory, "startup.png"), ImageFormat.Png); } - BeginProductionComplexPlot(form, outputDirectory); + // Mark the transition before pumping nested X11 events during the click. complexRequested = true; complexStartedAt = watch.Elapsed.TotalSeconds; + BeginProductionComplexPlot(form, outputDirectory); } catch (Exception error) { @@ -171,8 +174,59 @@ private static void BeginProductionComplexPlot(Form form, string outputDirectory using (var before = CaptureX11Pixels(chart)) before.Save(Path.Combine(outputDirectory, "production-complex-before.png"), ImageFormat.Png); expression.Text = "z²"; - draw.PerformClick(); - Console.WriteLine("Production executable complex UI Draw was clicked; bounds=" + chart.Bounds); + var clickCount = 0; + draw.Click += (sender, args) => clickCount++; + // Button.PerformClick() is not a substitute for a native Mono/X11 + // pointer action. The previous probe could remain blank even though + // the bootstrapped desktop's XTEST Draw action painted successfully. + ClickProductionDrawOnX11(draw); + if (clickCount != 1) + throw new InvalidOperationException("X11 Draw did not reach the production button Click event; count=" + clickCount); + Console.WriteLine("Production executable real X11 Draw clicked once; bounds=" + chart.Bounds + + "; expression=" + expression.Text + "; " + DescribeComplexFunctionState(chart)); + } + + private static string DescribeComplexFunctionState(Control chart) + { + var function = chart.GetType().GetField("function", BindingFlags.Instance | BindingFlags.NonPublic); + var error = chart.GetType().GetProperty("LastRenderError", BindingFlags.Instance | BindingFlags.Public); + return "function-assigned=" + (function != null && function.GetValue(chart) != null) + + "; async-error=" + (error == null ? "(unavailable)" : (error.GetValue(chart, null) ?? "(none)")); + } + + [DllImport("libX11.so.6")] + private static extern IntPtr XOpenDisplay(IntPtr displayName); + [DllImport("libX11.so.6")] + private static extern int XCloseDisplay(IntPtr display); + [DllImport("libX11.so.6")] + private static extern int XSync(IntPtr display, int discard); + [DllImport("libXtst.so.6")] + private static extern int XTestFakeMotionEvent(IntPtr display, int screen, int x, int y, ulong delay); + [DllImport("libXtst.so.6")] + private static extern int XTestFakeButtonEvent(IntPtr display, uint button, int press, ulong delay); + + private static void ClickProductionDrawOnX11(Button draw) + { + var center = draw.PointToScreen(new Point(draw.Width / 2, draw.Height / 2)); + var display = XOpenDisplay(IntPtr.Zero); + if (display == IntPtr.Zero) + throw new InvalidOperationException("Cannot open X11 display for production Draw acceptance."); + try + { + if (XTestFakeMotionEvent(display, 0, center.X, center.Y, 0) == 0) + throw new InvalidOperationException("Cannot position native pointer over production Draw."); + XSync(display, 0); + Application.DoEvents(); + if (XTestFakeButtonEvent(display, 1, 1, 0) == 0) + throw new InvalidOperationException("Native X11 Draw press failed."); + XSync(display, 0); + Application.DoEvents(); + if (XTestFakeButtonEvent(display, 1, 0, 0) == 0) + throw new InvalidOperationException("Native X11 Draw release failed."); + XSync(display, 0); + Application.DoEvents(); + } + finally { XCloseDisplay(display); } } private static Bitmap CaptureX11Pixels(Control control) From 7965e05d35a656a4671abb4caf8e36976eabb3fb Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Tue, 27 Oct 2026 00:29:58 +0100 Subject: [PATCH 16/20] fix(mono): retain Windows UI exception contract for current complex redraw failures --- Computator.NET.Charting/ComplexCharting/ComplexChart.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Computator.NET.Charting/ComplexCharting/ComplexChart.cs b/Computator.NET.Charting/ComplexCharting/ComplexChart.cs index df808263..fcd15c40 100644 --- a/Computator.NET.Charting/ComplexCharting/ComplexChart.cs +++ b/Computator.NET.Charting/ComplexCharting/ComplexChart.cs @@ -504,16 +504,19 @@ public async void Redraw() // Superseded evaluations should never report an error. continue; } +#if __MonoCS__ catch (Exception exception) { - // A failed background evaluation must not tear down the - // WinForms UI via this async-void event-path method. - // Keep the control usable and show a visible diagnostic. + // Mono: keep async-void errors inside the chart to avoid + // killing the production message loop. The diagnostic is + // painted on screen and a later Draw can recover. + // Windows retains its established ThreadException route. Logger.Error(exception, "Complex chart asynchronous redraw failed."); LastRenderError = exception.Message; Invalidate(); break; } +#endif if (function == null || IsDisposed || version != _redrawVersion) { samples.Bitmap.Dispose(); From 9549148e7d04bbafbaab963c4e7c7b5c1f7509d9 Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Tue, 27 Oct 2026 00:32:40 +0100 Subject: [PATCH 17/20] test(mono): avoid duplicate X11 imports when native 3D support is integrated --- .../SmokeTests/ComplexChartUiAcceptance.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs index cf6af87e..1cb658fa 100644 --- a/build/mono/SmokeTests/ComplexChartUiAcceptance.cs +++ b/build/mono/SmokeTests/ComplexChartUiAcceptance.cs @@ -269,36 +269,36 @@ private static int CountDomainColorPixels(Bitmap bitmap) } return count; } - [DllImport("libX11.so.6")] - private static extern IntPtr XOpenDisplay(IntPtr displayName); - [DllImport("libX11.so.6")] - private static extern int XCloseDisplay(IntPtr display); - [DllImport("libX11.so.6")] - private static extern int XSync(IntPtr display, int discard); - [DllImport("libXtst.so.6")] - private static extern int XTestFakeMotionEvent(IntPtr display, int screen, int x, int y, ulong delay); - [DllImport("libXtst.so.6")] - private static extern int XTestFakeButtonEvent(IntPtr display, uint button, int press, ulong delay); + [DllImport("libX11.so.6", EntryPoint = "XOpenDisplay")] + private static extern IntPtr ComplexXOpenDisplay(IntPtr displayName); + [DllImport("libX11.so.6", EntryPoint = "XCloseDisplay")] + private static extern int ComplexXCloseDisplay(IntPtr display); + [DllImport("libX11.so.6", EntryPoint = "XSync")] + private static extern int ComplexXSync(IntPtr display, int discard); + [DllImport("libXtst.so.6", EntryPoint = "XTestFakeMotionEvent")] + private static extern int ComplexXTestFakeMotionEvent(IntPtr display, int screen, int x, int y, ulong delay); + [DllImport("libXtst.so.6", EntryPoint = "XTestFakeButtonEvent")] + private static extern int ComplexXTestFakeButtonEvent(IntPtr display, uint button, int press, ulong delay); private static void ClickActualX11Button(Control button) { var center = button.PointToScreen(new Point(button.Width / 2, button.Height / 2)); - var display = XOpenDisplay(IntPtr.Zero); + var display = ComplexXOpenDisplay(IntPtr.Zero); Check(display != IntPtr.Zero, "Cannot open native X11 display for Draw mouse input."); try { - Check(XTestFakeMotionEvent(display, 0, center.X, center.Y, 0) != 0, + Check(ComplexXTestFakeMotionEvent(display, 0, center.X, center.Y, 0) != 0, "Native mouse motion to Draw button failed."); - XSync(display, 0); + ComplexXSync(display, 0); Application.DoEvents(); - Check(XTestFakeButtonEvent(display, 1, 1, 0) != 0, "X11 Draw mouse press failed."); - XSync(display, 0); + Check(ComplexXTestFakeButtonEvent(display, 1, 1, 0) != 0, "X11 Draw mouse press failed."); + ComplexXSync(display, 0); Application.DoEvents(); - Check(XTestFakeButtonEvent(display, 1, 0, 0) != 0, "X11 Draw mouse release failed."); - XSync(display, 0); + Check(ComplexXTestFakeButtonEvent(display, 1, 0, 0) != 0, "X11 Draw mouse release failed."); + ComplexXSync(display, 0); Application.DoEvents(); } - finally { XCloseDisplay(display); } + finally { ComplexXCloseDisplay(display); } } } From 35e8c5cd824422aea0ac64e46516b144d1cdd722 Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Tue, 27 Oct 2026 00:50:52 +0100 Subject: [PATCH 18/20] test(mono): distinguish stalled complex redraw from image publication and X11 repaint --- build/mono/StartupProbe/Program.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/build/mono/StartupProbe/Program.cs b/build/mono/StartupProbe/Program.cs index ffcdfddc..e5563ce6 100644 --- a/build/mono/StartupProbe/Program.cs +++ b/build/mono/StartupProbe/Program.cs @@ -190,8 +190,24 @@ private static string DescribeComplexFunctionState(Control chart) { var function = chart.GetType().GetField("function", BindingFlags.Instance | BindingFlags.NonPublic); var error = chart.GetType().GetProperty("LastRenderError", BindingFlags.Instance | BindingFlags.Public); + var type = chart.GetType(); + var redrawing = type.GetField("_redrawing", BindingFlags.Instance | BindingFlags.NonPublic); + var points = type.GetField("_pointsValues", BindingFlags.Instance | BindingFlags.NonPublic); + var cache = type.GetField("image", BindingFlags.Instance | BindingFlags.NonPublic); + var version = type.GetField("_redrawVersion", BindingFlags.Instance | BindingFlags.NonPublic); + var quality = type.GetField("quality", BindingFlags.Instance | BindingFlags.NonPublic); + var sampled = points == null ? null : points.GetValue(chart) as Array; + var cached = cache == null ? null : cache.GetValue(chart) as Bitmap; return "function-assigned=" + (function != null && function.GetValue(chart) != null) + - "; async-error=" + (error == null ? "(unavailable)" : (error.GetValue(chart, null) ?? "(none)")); + "; async-error=" + (error == null ? "(unavailable)" : (error.GetValue(chart, null) ?? "(none)")) + + "; redraw-active=" + (redrawing == null ? "(unavailable)" : redrawing.GetValue(chart).ToString()) + + "; redraw-version=" + (version == null ? "(unavailable)" : version.GetValue(chart).ToString()) + + "; sample-quality=" + (quality == null ? "(unavailable)" : quality.GetValue(chart).ToString()) + + "; sampled-grid=" + (sampled == null ? "(none)" : sampled.GetLength(0) + "x" + sampled.GetLength(1)) + + "; cached-image=" + (cached == null ? "(none)" : cached.Width + "x" + cached.Height) + + "; cached-colored-pixels=" + (cached == null ? -1 : CountDomainColorPixels(cached)) + + "; current-sync-context=" + (System.Threading.SynchronizationContext.Current == null + ? "(none)" : System.Threading.SynchronizationContext.Current.GetType().FullName); } [DllImport("libX11.so.6")] From eeb395e873c794ba0d32da42b8e84f0985f464d1 Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Tue, 27 Oct 2026 01:03:13 +0100 Subject: [PATCH 19/20] fix(mono): publish complex chart samples via UI timer instead of stalled WinForms await continuation --- .../ComplexCharting/ComplexChart.cs | 132 ++++++++++++++++-- 1 file changed, 119 insertions(+), 13 deletions(-) diff --git a/Computator.NET.Charting/ComplexCharting/ComplexChart.cs b/Computator.NET.Charting/ComplexCharting/ComplexChart.cs index fcd15c40..88c42463 100644 --- a/Computator.NET.Charting/ComplexCharting/ComplexChart.cs +++ b/Computator.NET.Charting/ComplexCharting/ComplexChart.cs @@ -36,6 +36,14 @@ private void OnPropertyChanged(string propertyName) // private Task RedrawTask; private ComplexPoint[,] _pointsValues; private bool _redrawing = false; +#if __MonoCS__ + // The real Mono Application.Run pipeline may never dispatch the awaited + // WinForms synchronization-context continuation. Poll completion from + // the UI message loop instead; never access WinForms from the worker. + private System.Windows.Forms.Timer _monoRenderTimer; + private Task _monoRenderTask; + private long _monoRenderTaskVersion; +#endif public string LastRenderError { get; private set; } private long _redrawVersion; private Function function; @@ -291,6 +299,22 @@ protected override void Dispose(bool disposing) { if (disposing) { +#if __MonoCS__ + // A task may finish after the control has closed; release its + // independent sample bitmap without posting into a dead form. + _monoRenderTimer?.Stop(); + _monoRenderTimer?.Dispose(); + _monoRenderTimer = null; + var pending = _monoRenderTask; + _monoRenderTask = null; + if (pending != null) + pending.ContinueWith(t => + { + if (t.Status == TaskStatus.RanToCompletion) + t.Result.Bitmap.Dispose(); + }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); +#endif ++_redrawVersion; function = null; _pointsValues = null; @@ -478,6 +502,98 @@ public void ShowPlotDialog() plotForm.Show(); } +#if __MonoCS__ + public void Redraw() + { + if (IsDisposed || Disposing) + return; + ++_redrawVersion; + if (function == null || DrawWidth <= 0 || DrawHeight <= 0 || _redrawing) + return; + _redrawing = true; + Cursor = Cursors.WaitCursor; + if (_monoRenderTimer == null) + { + _monoRenderTimer = new System.Windows.Forms.Timer { Interval = 35 }; + _monoRenderTimer.Tick += MonoRenderTimerTick; + } + StartMonoRenderTask(); + } + + private void StartMonoRenderTask() + { + _monoRenderTaskVersion = _redrawVersion; + var width = DrawWidth; + var height = DrawHeight; + // Use the default scheduler and a dedicated worker on Mono. This + // avoids starvation by UI-associated custom schedulers and never + // executes the expensive sampling on the WinForms thread. + _monoRenderTask = Task.Factory.StartNew( + () => CalculateValuesAndColors(width, height), CancellationToken.None, + TaskCreationOptions.LongRunning, TaskScheduler.Default); + _monoRenderTimer.Start(); + } + + private void MonoRenderTimerTick(object sender, EventArgs e) + { + var task = _monoRenderTask; + if (task == null || !task.IsCompleted) + return; + _monoRenderTask = null; + var version = _monoRenderTaskVersion; + try + { + if (task.Status == TaskStatus.RanToCompletion) + { + var samples = task.Result; + if (!IsDisposed && !Disposing && function != null && + version == _redrawVersion) + { + var oldImage = image; + image = samples.Bitmap; + _pointsValues = samples.PointsValues; + oldImage?.Dispose(); + Invalidate(); + } + else + { + samples.Bitmap.Dispose(); + } + } + else if (task.IsFaulted && !IsDisposed && !Disposing && + function != null && version == _redrawVersion) + { + var error = task.Exception.GetBaseException(); + Logger.Error(error, "Complex chart asynchronous redraw failed."); + LastRenderError = error.Message; + Invalidate(); + } + } + catch (Exception error) + { + // Keep the actual production message loop alive on Mono. + Logger.Error(error, "Complex chart result publication failed."); + LastRenderError = error.Message; + if (!IsDisposed && !Disposing) + Invalidate(); + } + + if (!IsDisposed && !Disposing && function != null && + DrawWidth > 0 && DrawHeight > 0 && version != _redrawVersion) + { + // Coalesce edits, Clear, mode changes and resizing while a + // previous evaluation is still running. Never publish stale + // samples or stale errors from superseded work. + StartMonoRenderTask(); + return; + } + + _monoRenderTimer.Stop(); + _redrawing = false; + if (!IsDisposed && !Disposing) + Cursor = Cursors.Default; + } +#else public async void Redraw() { if (IsDisposed || Disposing) @@ -504,19 +620,7 @@ public async void Redraw() // Superseded evaluations should never report an error. continue; } -#if __MonoCS__ - catch (Exception exception) - { - // Mono: keep async-void errors inside the chart to avoid - // killing the production message loop. The diagnostic is - // painted on screen and a later Draw can recover. - // Windows retains its established ThreadException route. - Logger.Error(exception, "Complex chart asynchronous redraw failed."); - LastRenderError = exception.Message; - Invalidate(); - break; - } -#endif + if (function == null || IsDisposed || version != _redrawVersion) { samples.Bitmap.Dispose(); @@ -538,6 +642,8 @@ public async void Redraw() } } +#endif + protected override void OnPaint(PaintEventArgs pe) { PaintOn(pe.Graphics, ClientRectangle, image); From 82e576d2cd843de6721065a12986487831951dff Mon Sep 17 00:00:00 2001 From: "Pawel Troka, PhD Eng" Date: Tue, 27 Oct 2026 01:03:32 +0100 Subject: [PATCH 20/20] test(mono): report production worker completion and GUI polling state --- build/mono/StartupProbe/Program.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build/mono/StartupProbe/Program.cs b/build/mono/StartupProbe/Program.cs index e5563ce6..5547b22e 100644 --- a/build/mono/StartupProbe/Program.cs +++ b/build/mono/StartupProbe/Program.cs @@ -196,12 +196,18 @@ private static string DescribeComplexFunctionState(Control chart) var cache = type.GetField("image", BindingFlags.Instance | BindingFlags.NonPublic); var version = type.GetField("_redrawVersion", BindingFlags.Instance | BindingFlags.NonPublic); var quality = type.GetField("quality", BindingFlags.Instance | BindingFlags.NonPublic); + var monoTask = type.GetField("_monoRenderTask", BindingFlags.Instance | BindingFlags.NonPublic); + var monoTimer = type.GetField("_monoRenderTimer", BindingFlags.Instance | BindingFlags.NonPublic); + var pending = monoTask == null ? null : monoTask.GetValue(chart) as System.Threading.Tasks.Task; + var polling = monoTimer == null ? null : monoTimer.GetValue(chart) as System.Windows.Forms.Timer; var sampled = points == null ? null : points.GetValue(chart) as Array; var cached = cache == null ? null : cache.GetValue(chart) as Bitmap; return "function-assigned=" + (function != null && function.GetValue(chart) != null) + "; async-error=" + (error == null ? "(unavailable)" : (error.GetValue(chart, null) ?? "(none)")) + "; redraw-active=" + (redrawing == null ? "(unavailable)" : redrawing.GetValue(chart).ToString()) + "; redraw-version=" + (version == null ? "(unavailable)" : version.GetValue(chart).ToString()) + + "; mono-task-status=" + (pending == null ? "(none)" : pending.Status.ToString()) + + "; mono-timer-enabled=" + (polling == null ? "(none)" : polling.Enabled.ToString()) + "; sample-quality=" + (quality == null ? "(unavailable)" : quality.GetValue(chart).ToString()) + "; sampled-grid=" + (sampled == null ? "(none)" : sampled.GetLength(0) + "x" + sampled.GetLength(1)) + "; cached-image=" + (cached == null ? "(none)" : cached.Width + "x" + cached.Height) +