Refreshing a ViewportControl causes an ‘AccessViolationException’

Hi there,

My RhinoCommon plugin has been suffering from very intermittent crashes caused by an AccessViolationException getting thrown when refreshing a certain panel that contains a ViewportControl. I’ve finally managed to reproduce it fairly reliably in a very minimal example, and it seems to be a bug with the ViewportControl.

Here’s part of the stacktrace when this happened with a debugger attached:

rhcommon_c.dll!00007ffa674bb1ac()
[Managed to Native Transition]
RhinoWindows.dll!RhinoWindows.Forms.Controls.ViewportControl.OnPaint(System.Windows.Forms.PaintEventArgs e)
System.Windows.Forms.dll!System.Windows.Forms.Control.PaintWithErrorHandling(System.Windows.Forms.PaintEventArgs e, short layer)
System.Windows.Forms.dll!System.Windows.Forms.Control.WmPaint(ref System.Windows.Forms.Message m)
System.Windows.Forms.dll!System.Windows.Forms.Control.WndProc(ref System.Windows.Forms.Message m)
System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.Callback(Windows.Win32.Foundation.HWND hWnd, Windows.Win32.MessageId msg, Windows.Win32.Foundation.WPARAM wparam, Windows.Win32.Foundation.LPARAM lparam)
[Native to Managed Transition]
user32.dll!00007ffbf97bc396()
user32.dll!00007ffbf97bbe5c()
Microsoft.VisualStudio.Debugger.Runtime.Impl.dll!00007ffbaf5b1bb8()
opengl32.dll!00007ffb221ebe23()
user32.dll!00007ffbf97bc396()
user32.dll!00007ffbf97bbc1c()
user32.dll!00007ffbf97f22a3()
ntdll.dll!00007ffbf9de5cc4()
win32u.dll!00007ffbf702cb04()
[Managed to Native Transition]
System.Windows.Forms.Primitives.dll!Windows.Win32.PInvoke.UpdateWindow<System.__Canon>(System.__Canon hWnd)
System.Windows.Forms.dll!System.Windows.Forms.Control.Refresh()
RhinoWindows.dll!RhinoWindows.Forms.Controls.ViewportControlHandler.Refresh()
Rhino.UI.dll!Rhino.UI.Controls.ViewportControl.Refresh()

The basic description is that I want a panel that has a viewport inside it. Within my plugin I can mutate values that change what will be drawn inside the panel’s viewport. To have the viewport update with the changed information, I call Rhino.UI.Controls.ViewportControl.Refresh(). This call can randomly cause an uncatchable AccessViolationException to get thrown (very randomly and can be very infrequently).

My minimal example for reproducing below is made up of two things:

  • A RhinoCommon plugin csproj file and one source file
    • It registers one panel that contains a ViewportControl
    • It contains one command that starts a thread and repeatedly (with pauses) calls Refresh() on the viewport control
  • A small batch script called provoke-access-violation.bat that, in a loop, opens Rhino, runs the command described above and outputs if the Rhino exits in error.

Both are in the zip below:

ViewportPanelPlugin.zip (26.6 KB)

To reproduce the issue:

  1. Build the ViewportPanelPlugin.csproj
  2. Register the plugin’s output rhp file with Rhino (drag and drop into an open Rhino instance)
  3. Close Rhino
  4. Run the batch script
    1. This will open and close Rhino 100 times, running the RefreshCommand each time, and printing if Rhino exited early in any of the 100 iterations.

Here’s a recording of that script running, and finding it crashing on the 49’th iteration:

The Minimal Viewport Panel Plugin

using Eto.Forms;
using Rhino;
using Rhino.Commands;
using Rhino.Input;
using Rhino.Input.Custom;
using Rhino.PlugIns;
using Rhino.UI;
using Rhino.UI.Controls;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Command = Rhino.Commands.Command;

namespace ViewportPanelPlugin
{
    [Guid("5D290748-D453-456E-99F7-49DC558E8C0C")]    
    public class MyViewportPanel : Panel
    {
        public ViewportControl ViewportControl { get; }

        public MyViewportPanel()
        {
            ViewportControl viewportControl = new();
            ViewportControl = viewportControl;
            Content = viewportControl;
        }
    }

     public class ViewportPanelCommand : Command
    {
        public ViewportPanelCommand()
        {
            // Rhino only creates one instance of each command class defined in a
            // plug-in, so it is safe to store a refence in a static property.
            Instance = this;
        }

        ///<summary>The only instance of this command.</summary>
        public static ViewportPanelCommand Instance { get; private set; }

        ///<returns>The command name as it appears on the Rhino command line.</returns>
        public override string EnglishName => "RefreshCommand";

        protected override Result RunCommand(RhinoDoc doc, RunMode mode)
        {
            var count = new OptionInteger(1, 1, 10000);
            var gOpt = new GetOption();
            gOpt.AcceptEnterWhenDone(true);
            gOpt.AddOptionInteger("Count", ref count);
            gOpt.AcceptNothing(true);
            while (true)
            {
                var result = gOpt.Get();
                if (gOpt.CommandResult() == Result.Cancel)
                {
                    RhinoApp.WriteLine("Cancelled");
                    return Result.Cancel;
                }

                if (result == GetResult.Option)
                {
                    continue;
                }
                break;
            }

            Rhino.UI.Panels.OpenPanel(typeof(MyViewportPanel).GUID);

            Task.Run(() =>
            {
                int iterations = count.CurrentValue;
                for (int i = 1; i <= iterations; i++)
                {
                    var panel = Rhino.UI.Panels.GetPanel<MyViewportPanel>();
                    RhinoApp.InvokeOnUiThread(() =>
                    {
                        panel.ViewportControl.Refresh();
                    });
                    System.Threading.Thread.Sleep(100);
                    RhinoApp.WriteLine($"{i}/{iterations}");
                }
                RhinoApp.WriteLine($"Completed {iterations} iterations");
                RhinoDoc.ActiveDoc.Modified = false;
                RhinoApp.Exit(false);
            });
            
            return Result.Success;
        }
    }


    ///<summary>
    /// <para>Every RhinoCommon .rhp assembly must have one and only one PlugIn-derived
    /// class. DO NOT create instances of this class yourself. It is the
    /// responsibility of Rhino to create an instance of this class.</para>
    /// <para>To complete plug-in information, please also see all PlugInDescription
    /// attributes in AssemblyInfo.cs (you might need to click "Project" ->
    /// "Show All Files" to see it in the "Solution Explorer" window).</para>
    ///</summary>
    public class ViewportPanelPlugin : Rhino.PlugIns.PlugIn
    {
        public ViewportPanelPlugin()
        {
            Instance = this;
        }

        ///<summary>Gets the only instance of the ViewportPanelPlugin plug-in.</summary>
        public static ViewportPanelPlugin Instance { get; private set; }

        // You can override methods here to change the plug-in behavior on
        // loading and shut down, add options pages to the Rhino _Option command
        // and maintain plug-in wide options in a document.
        protected override LoadReturnCode OnLoad(ref string errorMessage)
        {
            Panels.RegisterPanel(Instance,
                typeof(MyViewportPanel),
                "Refreshing this can cause an AccessViolationException",
                null,
                PanelType.PerDoc
            );

            return LoadReturnCode.Success;
        }
    }
}

The Batch Script to provoke it through many iterations

@echo off

set Rhino="C:/Program Files/Rhino 8/System/Rhino.exe"
set script="-RefreshCommand Count=50 _Enter"
set RhinoScript=%Rhino% /nosplash /runscript=%script%

set total=100
SETLOCAL ENABLEDELAYEDEXPANSION
for /l %%x in (1, 1, %total%) do (
echo Running iteration %%x
%RhinoScript% || (
echo Failed: 'AccessViolationException' thrown
set /a err_count=err_count+1
)
)

echo Errored %err_count%\%total% times

Example output of the script

Here’s a particularly bad run of the script, where it crashed 4/20 times.

Looks like you are accessing UI from a background thread. Here is a quick analysis and proposed solution:

@jstevenson hey thanks for the reply, but doesn’t appear to be the issue.

I revised accordingly, putting GetPanel into the delegate passed to InvokeOnUiThread (which was just an error on my part when writing the minimal example, b/c my original code has GetPanel called inside the delegate), but the error still occurs.

Change

            Task.Run(() =>
            {
                int iterations = count.CurrentValue;
                for (int i = 1; i <= iterations; i++)
                {
                    RhinoApp.InvokeOnUiThread(() =>
                    {
                        var panel = Rhino.UI.Panels.GetPanel<MyViewportPanel>();
                        panel.ViewportControl.Refresh();
                    });
                    System.Threading.Thread.Sleep(100);
                    RhinoApp.WriteLine($"{i}/{iterations}");
                }
                RhinoApp.WriteLine($"Completed {iterations} iterations");
                RhinoDoc.ActiveDoc.Modified = false;
                RhinoApp.Exit(false);
            });

Test output

C:\Users\dcondon\Projects\PolarisCAM\bugs\ViewportPanelPlugin>provoke-access-violation.bat
Running iteration 1
Running iteration 2
Running iteration 3
Running iteration 4
Running iteration 5
Running iteration 6
Running iteration 7
Running iteration 8
Running iteration 9
Running iteration 10
Running iteration 11
Running iteration 12
Running iteration 13
Running iteration 14
Running iteration 15
Running iteration 16
Running iteration 17
Running iteration 18
Running iteration 19
Running iteration 20
Running iteration 21
Running iteration 22
Running iteration 23
Running iteration 24
Running iteration 25
Running iteration 26
Running iteration 27
Running iteration 28
Running iteration 29
Running iteration 30
Running iteration 31
Running iteration 32
Running iteration 33
Running iteration 34
Running iteration 35
Running iteration 36
Running iteration 37
Running iteration 38
Running iteration 39
Running iteration 40
Running iteration 41
Running iteration 42
Running iteration 43
Running iteration 44
Running iteration 45
Failed: 'AccessViolationException' thrown
Running iteration 46
Running iteration 47
Running iteration 48
Running iteration 49
Running iteration 50
Running iteration 51
Running iteration 52
Running iteration 53
Running iteration 54
Running iteration 55
Running iteration 56
Running iteration 57
Running iteration 58
Running iteration 59
Running iteration 60
Running iteration 61
Running iteration 62
Running iteration 63
Running iteration 64
Running iteration 65
Running iteration 66
Running iteration 67
Running iteration 68
Running iteration 69
Running iteration 70
Running iteration 71
Running iteration 72
Running iteration 73
Running iteration 74
Running iteration 75
Running iteration 76
Running iteration 77
Running iteration 78
Running iteration 79
Running iteration 80
Running iteration 81
Running iteration 82
Running iteration 83
Running iteration 84
Running iteration 85
Running iteration 86
Running iteration 87
Running iteration 88
Running iteration 89
Running iteration 90
Running iteration 91
Running iteration 92
Running iteration 93
Running iteration 94
Running iteration 95
Running iteration 96
Running iteration 97
Running iteration 98
Running iteration 99
Running iteration 100
Errored 1\100 times

Your original post, showed it errored 4 / 20 times. Which is 20% of the time. With my adjusted code your own test now shows it errored 1/100 times. So reduced the exception from 20% to 1%. So my change is most definitely required for your code.

I am attempting to run and diagnose the remaining issue, but so far it hasn’t errored for me at all.

The test is continuing to execute while I edit this post…

just 2 guesses:
(a)
what happens if you put the refresh logic into a static method of a static class ?
this would eliminate compiler magic around lambda / anonymous function.

(b)
what happens if you increase to Sleep(2000) - yes 2 seconds - drink a coffee…
I always wonder, why changing (main menu) window → window layout takes so long.

good luck and looking forward to see / learn the solution. happy coding - tom

@Dustin_Condon my code change does appear to fix the issue. I just completed your test. With my single code change of moving the panel inside the UI the test never fails:

Results:

**********************************************************************
** Visual Studio 2026 Developer PowerShell v18.6.2
** Copyright (c) 2026 Microsoft Corporation
**********************************************************************
PS C:\Users\jstev\Downloads\ViewportPanelPlugin> .\provoke-access-violation.bat
Running iteration 1
Running iteration 2
Running iteration 3
Running iteration 4
Running iteration 5
Running iteration 6
Running iteration 7
Running iteration 8
Running iteration 9
Running iteration 10
Running iteration 11
Running iteration 12
Running iteration 13
Running iteration 14
Running iteration 15
Running iteration 16
Running iteration 17
Running iteration 18
Running iteration 19
Running iteration 20
Running iteration 21
Running iteration 22
Running iteration 23
Running iteration 24
Running iteration 25
Running iteration 26
Running iteration 27
Running iteration 28
Running iteration 29
Running iteration 30
Running iteration 31
Running iteration 32
Running iteration 33
Running iteration 34
Running iteration 35
Running iteration 36
Running iteration 37
Running iteration 38
Running iteration 39
Running iteration 40
Running iteration 41
Running iteration 42
Running iteration 43
Running iteration 44
Running iteration 45
Running iteration 46
Running iteration 47
Running iteration 48
Running iteration 49
Running iteration 50
Running iteration 51
Running iteration 52
Running iteration 53
Running iteration 54
Running iteration 55
Running iteration 56
Running iteration 57
Running iteration 58
Running iteration 59
Running iteration 60
Running iteration 61
Running iteration 62
Running iteration 63
Running iteration 64
Running iteration 65
Running iteration 66
Running iteration 67
Running iteration 68
Running iteration 69
Running iteration 70
Running iteration 71
Running iteration 72
Running iteration 73
Running iteration 74
Running iteration 75
Running iteration 76
Running iteration 77
Running iteration 78
Running iteration 79
Running iteration 80
Running iteration 81
Running iteration 82
Running iteration 83
Running iteration 84
Running iteration 85
Running iteration 86
Running iteration 87
Running iteration 88
Running iteration 89
Running iteration 90
Running iteration 91
Running iteration 92
Running iteration 93
Running iteration 94
Running iteration 95
Running iteration 96
Running iteration 97
Running iteration 98
Running iteration 99
Running iteration 100
Errored \100 times
PS C:\Users\jstev\Downloads\ViewportPanelPlugin>

The test takes forever to run 100 instances of Rhino, each refreshing your window 50 times, so I’ didn’t try your original code to see if I get that same error as you. I suspect your results may vary dependant upon the resources (CPUs/Threads) available on your machine.

You don’t actually know that it is an AccessViolationException that is causing your issue, because your batch file is just echoing that info…

image

You may want some kind of additional logging, and a try/catch to see any other exceptions within that Task.Run. Your current code would be swallowing the exception in the Task. Something like this:

Thanks for trying it out! But just to be sure, I would see if you can see if fails at all in the original version too. Also it seems like having more than 100 iterations is necessary. Most times it’s only failing around 1/100. On the weekend I ran it for 10,000 iterations (with your proposed change), and it still failed 188 times.

...
Running iteration 9996
Running iteration 9997
Running iteration 9998
Running iteration 9999
Running iteration 10000
Errored 188\10000 times

So failure rate with a larger sample size seems to be around 2% of the time.. I’ve definitely seen this it fail on other machines, so I know it’s not just mine.

That’s a good idea to try and eliminate other types of exceptions for the test. I’ll try catching exception from refresh and ignoring them, so that the process won’t exit with failure in that case. Note that the exception I’m talking about (AccessViolationException), is intentionally not catchable and leads to the process exiting in failure ( System.AccessViolationException class - .NET | Microsoft Learn ), so that’s the one I’m focused on detecting.

There’s really no good way to log when this exception happens (to my knowledge) b/c it’s not like you can try/catch + log it. So I’m basically relying on the batch script logic of %RhinoScript% || (…) to catch it (if %RhinoScript% fails, the OR batch code runs and count an error as happening)

I wouldn’t pay too much attention to that as a statistic, b/c it’s a very low sample size. I’ll generate a better statistic by running the original code with 10,000 iterations too, and see if they differ significantly.

So I was able to run the 5400 iterations at the end of the day with the original code to get a better statistic (couldn’t do 10,000 during the week b/c it would take over 27 hours). I would say there is no significant difference.

Failures Total Average
Original code 87 5400 1.61%
Revised code 188 10000 1.88%

where

Original

var panel = Rhino.UI.Panels.GetPanel<MyViewportPanel>();                    
RhinoApp.InvokeOnUiThread(() =>
{
    panel.ViewportControl.Refresh();
});

Revised

RhinoApp.InvokeOnUiThread(() =>
{
    var panel = Rhino.UI.Panels.GetPanel<MyViewportPanel>();
    panel.ViewportControl.Refresh();
});

So I would say the issue isn’t calling GetPanel inside/outside the closure. I will run the test again using your idea of catching any other possible exceptions to eliminate them from causing the failure @jstevenson. And I’ll try getting rid of the closure and instead pass in a static function for refreshing the panel @Tom_P.

Edit:

Just to note, if someone wants to try running it a minimal amount of times to check if the AccessViolationException happens on their end, for a 99% chance of it failing at least once, it will take around 268 iterations. For a 99.9% chance it would take 402 iterations (that’s assuming the chance of an iteration failing is 1.7%).

Did this (catch an other exceptions and ignore them, and make a static RefreshPanel function) i.e.

            Task.Run(() =>
            {
                try
                {
                    int iterations = count.CurrentValue;
                    for (int i = 1; i <= iterations; i++)
                    {
                        RhinoApp.InvokeOnUiThread(RefreshPanel);
                        System.Threading.Thread.Sleep(100);
                        RhinoApp.WriteLine($"{i}/{iterations}");
                    }

                    RhinoApp.WriteLine($"Completed {iterations} iterations");
                    RhinoDoc.ActiveDoc.Modified = false;
                    RhinoApp.Exit(false);
                }
                catch (Exception e)
                {
                    // Just swallow the exception
                    // (assumes it's not AccessViolationException, b/c those are 
                    // uncatchable).
                }
            });
        public static void RefreshPanel()
        {
            var panel = Rhino.UI.Panels.GetPanel<MyViewportPanel>();
            panel.ViewportControl.Refresh();
        }

It errored 110/5400 times.

So in total

Failures Total Average
Original 87 5400 1.61%
GetPanel inside closure 188 10000 1.88%
try/catch in thread + static RefreshPanel 110 5400 2.03%

They all have pretty close failure rates. So I’m thinking this doesn’t have anything to do with the code in the example. It would be good to confirm if someone else can reproduce.

Hi,

I don’t think this is fully resolved yet.

Moving Panels.GetPanel<MyViewportPanel>() inside the UI-thread delegate is definitely required, but Dustin’s follow-up still shows a failure after that change, so I would not treat that as the complete fix.

There are two separate issues in the sample:

  1. Since the panel is registered as PanelType.PerDoc, use the document-specific overload: Panels.GetPanel<MyViewportPanel>(doc). The parameterless overload is obsolete and can return the wrong or ambiguous panel instance.

  2. I would avoid using ViewportControl.Refresh() as the update mechanism here. The stack trace shows it forcing an immediate WinForms paint through Control.Refresh() / UpdateWindow() into ViewportControl.OnPaint(). For a native viewport/OpenGL-backed control, that synchronous paint path is exactly where reentrancy or lifetime timing issues can show up as an AccessViolationException.

A safer redraw request would be:

RhinoApp.InvokeOnUiThread((Action)(() =>
{
    var panel = Rhino.UI.Panels.GetPanel<MyViewportPanel>(doc);
    var viewport = panel?.ViewportControl;

    if (viewport is null || viewport.IsDisposed || !viewport.Loaded)
        return;

    viewport.Invalidate(); // Schedule repaint; do not force immediate paint.
}));

If the test harness needs deterministic sequencing, I would use RhinoApp.InvokeAndWait for the UI-thread section, and avoid calling RhinoApp.Exit(false) until queued UI work has had a chance to drain.

If Invalidate() still eventually crashes inside ViewportControl.OnPaint, then I do not think this is just a plugin-side threading bug anymore. At that point it looks like a Rhino ViewportControl native paint/lifetime issue, and McNeel would probably need a crash dump or this minimal reproducible sample to investigate it.

Thanks for the experiment suggestion! I ran 10,000 iterations with this change, and it still throws the AccessViolationException at pretty close to the same rate.

i.e it failed 273/10000 times. Meaning an average of 2.73%.

So in total

Failures Total Average
Original 87 5400 1.61%
GetPanel inside closure 188 10000 1.88%
try/catch in thread + static RefreshPanel 110 5400 2.03%
Invalidate instead of Refresh 273 10000 2.73%

The changes included:

  1. Replacing ViewportControl.Refresh() w/ ViewportControl.Invalidate
  2. Checking the viewport isn’t null, Disposed, or not Loaded before interacting with it.
  3. Using Panels.GetPanel<...>(doc) instead of the obsolete version.
  4. Ensuring the UI queue is empty before running RhinoApp.Exit by enqueuing that function itself.

The Task

            Task.Run(() =>
            {
                try
                {
                    int iterations = count.CurrentValue;
                    for (int i = 1; i <= iterations; i++)
                    {
                        RhinoApp.InvokeOnUiThread(RefreshPanel);
                        System.Threading.Thread.Sleep(100);
                        RhinoApp.WriteLine($"{i}/{iterations}");
                    }

                    RhinoApp.WriteLine($"Completed {iterations} iterations");
                    RhinoApp.InvokeOnUiThread(() =>
                    {
                        RhinoDoc.ActiveDoc.Modified = false;
                        RhinoApp.Exit(false);
                    });
                }
                catch (Exception e)
                {
                    // Just swallow the exception
                    // (assumes it's not AccessViolationException, b/c those are 
                    // uncatchable).
                }
            });

The Refresh Code

        public static void RefreshPanel()
        {
            var panel = Panels.GetPanel<MyViewportPanel>(RhinoDoc.ActiveDoc);
            var viewport = panel?.ViewportControl;
            if (viewport is null || viewport.IsDisposed || !viewport.Loaded)
            {
                RhinoApp.WriteLine("Skipped Refresh call...");
                return;
            }

            panel.ViewportControl.Invalidate();
        }

See here for all the code of this minimal example as it is currently: GitHub - dcondon-pmdi/ViewportPanelPlugin: Minimal example to show AccessViolationException being thrown from ViewportControl in RhinoCommon · GitHub

Rhino doesn’t appear to create a crash dump file from AccessViolationExceptions (likely b/c they can’t be caught). However I’ve noticed if I build and run the project in .NET framework4.8 instead of net7.0, then Rhino will pop up a crash window when an iteration fails. I think this is b/c it was only recent versions that .NET no longer allows catching AccessViolationExceptions. So I’ll run the test in net48 and post that dump crash file.

Hi @Dustin_Condon,

Why do you need to refresh the viewport control with this frequency?

– Dale

Hi @dale

I don’t in practice refresh at this frequency. The iterations at this frequency are just to provoke it, b/c it happens in practice very infrequently. This was the only way I could reliably reproduce it.

Edit: In practice the refresh is tied to other user controls (i.e. a “Recompute” button). It also happens randomly there.

Hi @dale,

So I tried increasing the pause between the refresh calls, and it doesn’t appear to reduce the probability of the exception getting thrown. I managed to run 4197 iterations with a sleep of a half of a second between refresh/invalidate calls (instead of a 10th of a second). i.e.

                    int iterations = count.CurrentValue;
                    for (int i = 1; i <= iterations; i++)
                    {
                        RhinoApp.InvokeOnUiThread(RefreshPanel);
                        System.Threading.Thread.Sleep(500);
                        RhinoApp.WriteLine($"{i}/{iterations}");
                    }

And I reduced the total number of refresh calls in an iteration from 50 to 20. i.e.

@echo off

set Rhino="C:/Program Files/Rhino 8/System/Rhino.exe"
set script="-RefreshCommand Count=20 _Enter"
set RhinoScript=%Rhino% /nosplash /runscript=%script%

set total=5400
SETLOCAL ENABLEDELAYEDEXPANSION
for /l %%x in (1, 1, %total%) do (
echo Running iteration %%x
%RhinoScript% || (
echo Failed: 'AccessViolationException' thrown
set /a err_count=err_count+1
)
)

echo Errored %err_count%\%total% times

And the test failed 225/4197 times, which increased the average to 5.36%