Do not let a dropped frame kill the application - #173
Open
joseangelmt wants to merge 1 commit into
Open
Conversation
Maximizing a window over a slow GLRender callback kills the process. We hit it
repeatedly over three weeks with a large point cloud, in two different shapes:
System.InvalidOperationException: Cannot call this method while the image
is unlocked.
at System.Windows.Interop.D3DImage.AddDirtyRect(Int32Rect dirtyRect)
at OpenTK.Wpf.GLWpfControlRenderer.Render(DrawingContext drawingContext)
at System.Windows.UIElement.Arrange(Rect finalRect)
System.ArgumentOutOfRangeException: dirtyRect ('1593') must be less than
or equal to '870'. (Parameter 'dirtyRect')
at System.Windows.Interop.D3DImage.AddDirtyRect(Int32Rect dirtyRect)
at OpenTK.Wpf.GLWpfControlRenderer.Render(DrawingContext drawingContext)
at System.Windows.UIElement.Arrange(Rect finalRect)
Either one is fatal rather than merely ugly, because Render runs inside the
layout pass: there is no application code above it, so nothing can catch it.
Both are the same thing seen from two sides. Render read the D3dImage property
eight times, and ReallocateFramebufferIfNeeded assigns a brand new D3DImage --
and new FramebufferWidth/Height -- whenever the size changes, so the method
could lock one image and dirty another. Reading the property once instead of
eight times is what turned the first exception into the second: with the local,
AddDirtyRect gets the image we did lock, but the rectangle was still being
built from FramebufferWidth/Height, which by then belonged to the new
generation. An unlocked image and a rectangle that does not fit are the two
ways that mismatch can surface.
So, three changes:
- Take the image ONCE into a local and use that same reference throughout.
Lock, AddDirtyRect and Unlock only mean anything on one and the same
image.
- Build the dirty rectangle from the image's own PixelWidth/PixelHeight
rather than from FramebufferWidth/Height. That is what we mean anyway --
the whole image is what we just drew -- and it cannot be out of range by
construction. The two sources agreeing was an assumption, and the numbers
above are it not holding.
- Let AddDirtyRect fail without taking the process with it, and always give
the lock back in a finally. Dropping one frame is harmless: the next
OnRender is already queued and paints a correct one. Leaving the image
locked would not be, because then every later frame fails too. The catch
is only around AddDirtyRect, on purpose -- wrapping the whole render would
also swallow whatever the user's GLRender throws, and that is theirs to
see.
What this does NOT do is explain how the two generations interleave.
ReallocateFramebufferIfNeeded assigns FramebufferWidth/Height before it creates
the new render target and the new D3DImage, so there is a window in which they
disagree, but I have not reproduced the path that reaches this line inside it;
a layout pass cannot re-enter here, since the dispatcher is suspended during
layout. The mismatch is now logged with both sizes so a future report comes
with something to go on.
Verified both ways with a minimal WPF app that forces the mismatch from the
GLRender callback -- the only user code that runs inside Render, between
SetBackBuffer and AddDirtyRect -- while resizing the window in a loop. Without
these changes the process dies with the exception above; with them it survives
400 frames with the mismatch forced on every single one.
joseangelmt
force-pushed
the
fix/d3dimage-swapped-during-render
branch
from
August 5, 2026 14:26
8da5706 to
7f8acbf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Render can die with this, taking the whole process down:
It is fatal and not merely ugly because Render runs inside the layout pass:
there is no application code above it, so nothing can catch it. We hit it four
times in two weeks while maximizing a window over a large point cloud, whose
GLRender callback takes tens of milliseconds.
I could NOT establish how the image ends up unlocked. My first guess was a
layout pass re-entering mid-render and reallocating the image, but that turns
out to be impossible: the dispatcher is suspended during layout, and trying to
pump it there throws "Cannot perform this operation while dispatcher processing
is suspended". So this does not claim a cause.
What it does is make the outcome survivable, whatever the cause: AddDirtyRect
is allowed to fail and drop that frame, and Unlock always runs. The next
OnRender is already queued and paints a correct frame. Leaving the image locked
would be worse than the dropped frame, because every later frame fails too.
The catch is only around AddDirtyRect: wrapping the whole render would also
swallow whatever the user's GLRender throws, and that is theirs to see.
Render also takes the D3dImage property into a local instead of reading it
eight times. That is not the fix and I am not claiming it is one -- just one
fewer way for a method to work on two different objects.
Verified both ways, by forcing the exact condition with a temporary extra
Unlock() before AddDirtyRect, over 400 window resizes: without the catch the
process dies with the exception above; with it, it survives and keeps drawing.